diff --git a/customize.dist/messages.js b/customize.dist/messages.js index a28190005..4d62fc52e 100755 --- a/customize.dist/messages.js +++ b/customize.dist/messages.js @@ -134,7 +134,12 @@ define(req, function(AppConfig, Default, Language) { } }; - + // XXX + Messages.badges_admin = "Instance administrator"; + Messages.badges_moderator = "Instance moderator"; + Messages.badges_premium = "Premium user"; + Messages.badges_error = "Error while validating this user's data"; + Messages.profile_badges = "Badges"; return Messages; diff --git a/customize.dist/src/less2/include/badges.less b/customize.dist/src/less2/include/badges.less new file mode 100644 index 000000000..7264840b3 --- /dev/null +++ b/customize.dist/src/less2/include/badges.less @@ -0,0 +1,28 @@ +@import (reference) "./colortheme-all.less"; +@import (reference) "./variables.less"; +.badges_vars( + @width: 30px +) { + @badges-width: @width; +} +.badges_main(@width: 30px) { + --LessLoader_require: LessLoader_currentFile(); + .badges_vars(@width); + --badges-width: @badges-width; + --badges-font: ceil(@badges-width*2/3); +} +& { + .badges_vars(); + i.cp-badge { + width: var(--badges-width); + height: var(--badges-width); + box-sizing: border-box; + border: 1px solid @cryptpad_text_col; + border-radius: 50%; + display: inline-flex; + justify-content: center; + align-items: center; + font-size: var(--badges-font); + } +} + diff --git a/customize.dist/src/less2/include/toolbar.less b/customize.dist/src/less2/include/toolbar.less index d78fccade..b904d26d8 100644 --- a/customize.dist/src/less2/include/toolbar.less +++ b/customize.dist/src/less2/include/toolbar.less @@ -263,6 +263,14 @@ .cp-toolbar-userlist-friend { padding: 0; } + .cp-userlist-badge { + i { + color: @cryptpad_color_brand; + } + i.cp-badge-error { + color: @cryptpad_color_warn_red; + } + } } } } diff --git a/funding.json b/funding.json new file mode 100644 index 000000000..58189e512 --- /dev/null +++ b/funding.json @@ -0,0 +1,151 @@ +{ + "version": "v1.0.0", + "drips": { + "ethereum": { + "ownedBy": "0xa117114b3a22e038c1545043d610dbd4029355b1" + } + }, + "entity": { + "type": "organisation", + "role": "owner", + "name": "XWiki SAS", + "email": "funding@xwiki.com", + "phone": "", + "description": "XWiki SAS is a European Open Source organization, maintaining Open Source Collaboration projects, XWiki and CryptPad. We believe that Open Source projects are key to getting back control on software, and that companies are important to allow the maintenance of Open Source projects.", + "webpageUrl": { + "url": "https://xwiki.com", + "wellKnown": "https://xwiki.com/.well-known/funding-manifest-urls" + } + }, + "projects": [ + { + "guid": "cryptpad", + "name": "CryptPad", + "description": "CryptPad is an end-to-end encrypted Collaboration Platform focused on providing real-time editing tools while protecting privacy.", + "webpageUrl": { + "url": "https://cryptpad.org", + "wellKnown": "https://cryptpad.org/.well-known/funding-manifest-urls" + }, + "repositoryUrl": { + "url": "https://github.com/cryptpad/cryptpad" + }, + "licenses": [ + "spdx:AGPL-3.0-or-later" + ], + "tags": [ + "privacy", + "e2ee", + "collaboration", + "realtimeediting", + "office", + "drive" + ] + } + ], + "funding": { + "channels": [ + { + "guid": "opencollective-cryptpad", + "type": "payment-provider", + "address": "https://opencollective.com/cryptpad", + "description": "OpenCollective CryptPad Account" + }, + { + "guid": "bank", + "type": "bank", + "address": "Contact XWiki SAS", + "description": "Donation by Bank Transfer or other means with XWiki SAS" + } + ], + "plans": [ + { + "guid": "cryptpad-individual-helper", + "status": "active", + "name": "Individual Donator - Starter", + "description": "Individual Donator would like to help us fund the maintenance and further improvement of the software.\n\n35000 people donating this every year would be needed to fund the current team to allow CryptPad to be independent from outside grants.\n\nThis plans corresponds to:\n\n- an occasional user of CryptPad\n- a regular user with limited funds\n- somebody just wanting to give us a nudge", + "amount": 20, + "currency": "EUR", + "frequency": "yearly", + "channels": [ + "opencollective-cryptpad" + ] + }, + { + "guid": "cryptpad-individual-user", + "status": "active", + "name": "Individual Donator - User", + "description": "Individual Donator would like to help us fund the maintenance and further improvement of the software.\n\n14000 people donating this every year would be needed to fund the current team to allow CryptPad to be independent from outside grants.\n\nThis plans corresponds to:\n\n- an regular user of CryptPad\n- a privacy believer who would like to help us\n- a personal instance owner with limited funds", + "amount": 50, + "currency": "EUR", + "frequency": "yearly", + "channels": [ + "opencollective-cryptpad" + ] + }, + { + "guid": "cryptpad-individual-sponsor", + "status": "active", + "name": "Individual Donator - Sponsor", + "description": "Individual Donator would like to help us fund the maintenance and further improvement of the software.\n\n3500 people donating this every year would be needed to fund the current team to allow CryptPad to be independent from outside grants.\n\nThis plans corresponds to:\n\n- an regular user of CryptPad who really likes CryptPad\n- a privacy believer who wants to accelerate privacy online\n- a personal or community small size instance owner", + "amount": 200, + "currency": "EUR", + "frequency": "yearly", + "channels": [ + "opencollective-cryptpad" + ] + }, + { + "guid": "cryptpad-instanceowner-smallorg", + "status": "active", + "name": "Small Organization Using CryptPad", + "description": "Small Organization which would like to help us fund the maintenance and further improvement of the software.\n\n1500 organizations donating this every year would be needed to fund the current team to allow CryptPad to be independent from outside grants.\n\nThis plans corresponds to:\n\n- a non-profit organization having some funds hosting CryptPad for their members\n- a small company using CryptPad for its users\n- any organizations wanting Privacy software to progress", + "amount": 500, + "currency": "EUR", + "frequency": "yearly", + "channels": [ + "opencollective-cryptpad" + ] + }, + { + "guid": "cryptpad-instanceowner-bigorg", + "status": "active", + "name": "Small Organization Using CryptPad", + "description": "Small Organization which would like to help us fund the maintenance and further improvement of the software.\n\n350 organizations donating this every year would be needed to fund the current team to allow CryptPad to be independent from outside grants.\n\nThis plans corresponds to:\n\n- a commercial company using CryptPad regularly internally or externally\n- any organization having sufficient funds wanting to support Open Source Software doing the right thing\n", + "amount": 2000, + "currency": "EUR", + "frequency": "yearly", + "channels": [ + "opencollective-cryptpad", + "bank" + ] + }, + { + "guid": "cryptpad-instanceowner-bigorg", + "status": "active", + "name": "Organization using CryptPad heavily or as part of their offering", + "description": "Organization using CryptPad heavily or as part of it's commercial offering and which would like to significantly contribute to it's development.\n\n70 organizations donating this every year would be needed to fund the current team to allow CryptPad to be independent from outside grants.\n\nThis plans corresponds to:\n\n- a commercial company using CryptPad heavily or as part of a commercial offering\n- foundations who would like to support the development.", + "amount": 10000, + "currency": "EUR", + "frequency": "yearly", + "channels": [ + "opencollective-cryptpad", + "bank" + ] + }, + { + "guid": "cryptpad-org-funder", + "status": "active", + "name": "Funding organization or large company supporting the maintenance and development of CryptPad", + "description": "This is meant for an organization investing in Privacy Preserving technologies and/or Open Source software which is important for the internet and wanting to support CryptPad with a grant. The grant can be for specific development useful for our roadmap or just for maintenance of the software.\n\n12 such grants would be needed to fund the current team.", + "amount": 50000, + "currency": "EUR", + "frequency": "one-time", + "channels": [ + "opencollective-cryptpad", + "bank" + ] + } + ], + "history": [] + } + } diff --git a/install-onlyoffice.sh b/install-onlyoffice.sh index da42811af..bdadd6c6b 100755 --- a/install-onlyoffice.sh +++ b/install-onlyoffice.sh @@ -15,6 +15,10 @@ PROPS_FILE="$CONF_DIR"/onlyoffice.properties declare -A PROPS main() { + + # clean build env in case a previous build has failed + rm -rf "$BUILDS_DIR" + mkdir -p "$CONF_DIR" load_props @@ -23,18 +27,75 @@ main() { ask_for_license - # Remeber the 1st version that is installed. This will help us install only + # Remember the 1st version that is installed. This will help us install only # needed OnlyOffice versions in a later version of this script. set_prop oldest_needed_version v1 mkdir -p "$OO_DIR" - install_version v1 4f370beb - install_version v2b d9da72fd - install_version v4 6ebc6938 - install_version v5 88a356f0 - install_version v6 abd8a309 - install_version v7 e1267803 - install_x2t v7.3+1 ab0c05b0e4c81071acea83f0c6a8e75f5870c360ec4abc4af09105dd9b52264af9711ec0b7020e87095193ac9b6e20305e446f2321a541f743626a598e5318c1 + + echo "Do you want to install all available OnlyOffice versions? (Y/N)" + read -r install_all + + declare -a selected_versions + + if [[ "$install_all" =~ ^[Yy] ]]; then + selected_versions=(v1 v2b v4 v5 v6 v7 v8 x2t) + else + echo "Select which versions to install (space-separated), options are:" + echo " v1 v2b v4 v5 v6 v7 v8 x2t" + read -ra selected_versions + fi + + for version in "${selected_versions[@]}"; do + case $version in + v1) + set_prop oldest_needed_version v1; install_old_version v1 4f370beb + # We delete 'help' from previous versions as they are useless and take up storage + rm -rf "$OO_DIR/v1/web-apps/apps/documenteditor/main/resources/help" + rm -rf "$OO_DIR/v1/web-apps/apps/presentationeditor/main/resources/help" + rm -rf "$OO_DIR/v1/web-apps/apps/spreadsheeteditor/main/resources/help" + ;; + v2b) + install_old_version v2b d9da72fd + rm -rf "$OO_DIR/v2b/web-apps/apps/documenteditor/main/resources/help" + rm -rf "$OO_DIR/v2b/web-apps/apps/presentationeditor/main/resources/help" + rm -rf "$OO_DIR/v2b/web-apps/apps/spreadsheeteditor/main/resources/help" + ;; + v4) + install_old_version v4 6ebc6938 + rm -rf "$OO_DIR/v4/web-apps/apps/documenteditor/main/resources/help" + rm -rf "$OO_DIR/v4/web-apps/apps/presentationeditor/main/resources/help" + rm -rf "$OO_DIR/v4/web-apps/apps/spreadsheeteditor/main/resources/help" + ;; + v5) + install_old_version v5 88a356f0 + rm -rf "$OO_DIR/v5/web-apps/apps/documenteditor/main/resources/help" + rm -rf "$OO_DIR/v5/web-apps/apps/presentationeditor/main/resources/help" + rm -rf "$OO_DIR/v5/web-apps/apps/spreadsheeteditor/main/resources/help" + ;; + v6) + install_old_version v6 abd8a309 + rm -rf "$OO_DIR/v6/web-apps/apps/documenteditor/main/resources/help" + rm -rf "$OO_DIR/v6/web-apps/apps/presentationeditor/main/resources/help" + rm -rf "$OO_DIR/v6/web-apps/apps/spreadsheeteditor/main/resources/help" + ;; + v7) + install_version v7 v7.3.3.60+10 be3c926d534b0c77aa1ed83b170a38b9e56ebbbfd73a1a968f523d63c5ed69ecdb063fad08740812de82c1e79af7309faf67ffdd81b45451adce539192f3414c + rm -rf "$OO_DIR/v7/web-apps/apps/documenteditor/main/resources/help" + rm -rf "$OO_DIR/v7/web-apps/apps/presentationeditor/main/resources/help" + rm -rf "$OO_DIR/v7/web-apps/apps/spreadsheeteditor/main/resources/help" + rm -rf "$OO_DIR/v7/web-apps/apps/common/main/resources/help/" + # From all the older versions only v7 has 'dictionaries', we remove it for the same reasons + rm -rf "$OO_DIR/v7/dictionaries/" + ;; + v8) install_version v8 v8.3.0.83+3 fbf77671ea8b960b6c65fdad025980894dfc9ed0b79d9540951f56c79871d0bde3361f29a35b9653764f7802ec74df01601b6c9961e0f3118988422667563470 ;; + x2t) install_x2t v7.3+1 ab0c05b0e4c81071acea83f0c6a8e75f5870c360ec4abc4af09105dd9b52264af9711ec0b7020e87095193ac9b6e20305e446f2321a541f743626a598e5318c1 ;; + *) + echo "Unknown version: $version" + exit 1 + ;; + esac + done rm -rf "$BUILDS_DIR" @@ -156,23 +217,22 @@ EOF } ensure_oo_is_downloaded() { - ensure_command_available git + ensure_command_available git - if ! [ -d "$BUILDS_DIR" ]; then - echo "Downloading OnlyOffice..." - git clone --bare https://github.com/cryptpad/onlyoffice-builds.git "$BUILDS_DIR" - fi - if [ ${TRUST_REPOSITORY+x} ] || [ "${PROPS[trust_repository]:-no}" == yes ]; then - git config --global --add safe.directory /cryptpad/onlyoffice-conf/onlyoffice-builds.git - fi + if ! [ -d "$BUILDS_DIR" ]; then + echo "Downloading OnlyOffice..." + git clone --bare https://github.com/cryptpad/onlyoffice-builds.git "$BUILDS_DIR" + fi + if [ ${TRUST_REPOSITORY+x} ] || [ "${PROPS[trust_repository]:-no}" == yes ]; then + git config --global --add safe.directory /cryptpad/onlyoffice-conf/onlyoffice-builds.git + fi } -install_version() { +install_old_version() { local DIR=$1 local COMMIT=$2 local FULL_DIR=$OO_DIR/$DIR - local LAST_DIR - LAST_DIR=$(pwd) + local LAST_DIR=$(pwd) local ACTUAL_COMMIT="not installed" if [ -e "$FULL_DIR"/.commit ]; then @@ -206,6 +266,40 @@ install_version() { fi } +install_version() { + ensure_command_available curl + ensure_command_available sha512sum + ensure_command_available unzip + + local DIR=$1 + local VERSION=$2 + local HASH=$3 + local FULL_DIR=$OO_DIR/$DIR + local LAST_DIR=$(pwd) + + if [ ! -e "$FULL_DIR"/.version ] || [ "$(cat "$FULL_DIR"/.version)" != "$VERSION" ]; then + rm -rf "$FULL_DIR" + mkdir -p "$FULL_DIR" + + cd "$FULL_DIR" + + curl "https://github.com/cryptpad/onlyoffice-editor/releases/download/$VERSION/onlyoffice-editor.zip" --location --output "onlyoffice-editor.zip" + echo "$HASH onlyoffice-editor.zip" >onlyoffice-editor.zip.sha512 + if ! sha512sum --check onlyoffice-editor.zip.sha512; then + echo "onlyoffice-editor.zip does not match expected checksum" + exit 1 + fi + unzip onlyoffice-editor.zip + rm onlyoffice-editor.zip* + + echo "$VERSION" >"$FULL_DIR"/.version + + echo "$DIR updated" + else + echo "$DIR was up to date" + fi +} + install_x2t() { local VERSION=$1 local HASH=$2 @@ -233,7 +327,6 @@ install_x2t() { ensure_command_available sha512sum ensure_command_available unzip curl "https://github.com/cryptpad/onlyoffice-x2t-wasm/releases/download/$VERSION/x2t.zip" --location --output x2t.zip - # curl "https://github.com/cryptpad/onlyoffice-x2t-wasm/releases/download/v7.3%2B1/x2t.zip" --location --output x2t.zip echo "$HASH x2t.zip" >x2t.zip.sha512 if ! sha512sum --check x2t.zip.sha512; then echo "x2t.zip does not match expected checksum" diff --git a/lib/commands/pin-rpc.js b/lib/commands/pin-rpc.js index 10a141745..f537f27c2 100644 --- a/lib/commands/pin-rpc.js +++ b/lib/commands/pin-rpc.js @@ -309,3 +309,9 @@ Pinning.getDeletedPads = function (Env, channels, cb) { Pinning.isChannelPinned = function (Env, channel, cb) { return void cb(void 0, true); }; + +Pinning.isPremium = function (Env, userKey, cb) { + const limit = Env.limits[userKey]; + return void cb(void 0, !!limit?.plan); + //return void cb(void 0, (limit?.plan && limit.plan !== "custom")); +}; diff --git a/lib/http-worker.js b/lib/http-worker.js index a0684dabf..90b399f16 100644 --- a/lib/http-worker.js +++ b/lib/http-worker.js @@ -18,6 +18,7 @@ const bodyParser = require('body-parser'); const BlobStore = require("./storage/blob"); const BlockStore = require("./storage/block"); const plugins = require("./plugin-manager"); +const gzipStatic = require('connect-gzip-static'); const DEFAULT_QUERY_TIMEOUT = 5000; const PID = process.pid; @@ -134,6 +135,7 @@ var applyHeaderMap = function (res, map) { var EXEMPT = [ /^\/common\/onlyoffice\/.*\.html.*/, + /^\/common\/onlyoffice\/dist\/.*\/sdkjs\/common\/spell\/spell\/spell.js.*/, // OnlyOffice loads spell.wasm in a way that needs unsave-eval /^\/(sheet|presentation|doc)\/inner\.html.*/, /^\/unsafeiframe\/inner\.html.*$/, ]; @@ -336,7 +338,7 @@ Object.keys(plugins || {}).forEach(name => { // serve custom app content from the customize directory // useful for testing pages customized with opengraph data app.use(Express.static(Path.resolve('./customize/www'))); -app.use(Express.static(Path.resolve('./www'))); +app.use(gzipStatic(Path.resolve('./www'))); var mainPages = Env.mainPages || Default.mainPages(); var mainPagePattern = new RegExp('^\/(' + mainPages.join('|') + ').html$'); diff --git a/lib/rpc.js b/lib/rpc.js index c28c5210a..806950db5 100644 --- a/lib/rpc.js +++ b/lib/rpc.js @@ -24,6 +24,7 @@ const UNAUTHENTICATED_CALLS = { WRITE_PRIVATE_MESSAGE: Channel.writePrivateMessage, DELETE_MAILBOX_MESSAGE: Channel.deleteMailboxMessage, GET_METADATA: Metadata.getMetadata, + IS_PREMIUM: Pinning.isPremium, ADD_FIRST_ADMIN: Admin.addFirstAdmin }; diff --git a/move-merge-src.js b/move-merge-src.js index f3c40bdc3..7ed5bd88f 100644 --- a/move-merge-src.js +++ b/move-merge-src.js @@ -20,7 +20,6 @@ const map = { './src/common/common-credential.js': './www/common/common-credential.js', './src/common/common-feedback.js': './www/common/common-feedback.js', './src/common/common-hash.js': './www/common/common-hash.js', - './src/common/common-messaging.js': './www/common/common-messaging.js', './src/common/common-realtime.js': './www/common/common-realtime.js', './src/common/common-signing-keys.js': './www/common/common-signing-keys.js', './src/common/common-util.js': './www/common/common-util.js', diff --git a/package-lock.json b/package-lock.json index 503ae5798..78f9d40b1 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "cryptpad", - "version": "2025.3.0", + "version": "2025.3.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "cryptpad", - "version": "2025.3.0", + "version": "2025.3.1", "license": "AGPL-3.0+", "dependencies": { "@mcrowe/minibloom": "^0.2.0", @@ -17,12 +17,13 @@ "bootstrap-tokenfield": "^0.12.0", "chainpad": "^5.2.6", "chainpad-crypto": "^0.3.0", - "chainpad-listmap": "^1.1.1", - "chainpad-netflux": "^1.2.3", + "chainpad-listmap": "^1.2.0", + "chainpad-netflux": "^1.3.0", "chainpad-server": "^5.2.4", "ckeditor": "npm:ckeditor4@~4.22.1", "codemirror": "^5.19.0", "components-font-awesome": "^4.6.3", + "connect-gzip-static": "^4.2.1", "cookie-parser": "^1.4.7", "croppie": "^2.5.0", "dragula": "3.7.2", @@ -1913,11 +1914,12 @@ } }, "node_modules/chainpad-listmap": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/chainpad-listmap/-/chainpad-listmap-1.1.1.tgz", - "integrity": "sha512-SXb3iJqWyp2wOMa9FT4kqOuR1KgjMPcNCzLlYignu1GdxMI67ZDGTLDucmht4CD8yuYzUWzWmJgs1vs/acH+rw==", + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/chainpad-listmap/-/chainpad-listmap-1.2.0.tgz", + "integrity": "sha512-PI7iwk/yKzEb1iaVmLKvMJYXjCjvIgUWzI07LeDGVvZPScnYbEzMso9YMQyxzjqrp8cyQxsL7sA8c/hZAz01cw==", + "license": "AGPL-3.0+", "dependencies": { - "chainpad-netflux": "^1.2.0", + "chainpad-netflux": "^1.3.0", "json.sortify": "~2.1.0" } }, @@ -1930,12 +1932,14 @@ } }, "node_modules/chainpad-netflux": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/chainpad-netflux/-/chainpad-netflux-1.2.3.tgz", - "integrity": "sha512-bXLUSxAsCu4kRnGorW+7fVofiCWSNGRkB+uFJ4kUV/zDJ3RJYU4J3+DZsL4rCTkLkAjgKt+YQCoYciCKLlTwqg==", + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/chainpad-netflux/-/chainpad-netflux-1.3.0.tgz", + "integrity": "sha512-oc47vLt78a/Fp7acQJvLNuX1JRrH9oOWAbzo+yOO1asCWDuI4oesPY81wHzVySKpo58GGfNpZN7CSTI8EZjYrQ==", "license": "LGPL-2.1", "dependencies": { - "netflux-websocket": "^1.2.0" + "netflux-websocket": "^1.2.0", + "tweetnacl": "^1.0.3", + "tweetnacl-util": "^0.15.1" } }, "node_modules/chainpad-server": { @@ -2063,6 +2067,22 @@ "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==" }, + "node_modules/connect-gzip-static": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/connect-gzip-static/-/connect-gzip-static-4.2.1.tgz", + "integrity": "sha512-6qC1NYUEZU6mftg2gPspcoPjgBzADm+fB1fPXu3RxBVe59P/Q9HQGQEHnuDaGRDJUoJwl5NX2OgoaaZp7RdpfQ==", + "license": "MIT", + "dependencies": { + "debug": "~2||~3||~4", + "mime-types": "~2", + "parseurl": "~1", + "send": "~0 || ~1", + "serve-static": "~1 || ~2" + }, + "engines": { + "node": ">= 20.1.0" + } + }, "node_modules/content-disposition": { "version": "0.5.4", "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", @@ -3270,9 +3290,10 @@ } }, "node_modules/http-proxy-middleware": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/http-proxy-middleware/-/http-proxy-middleware-3.0.3.tgz", - "integrity": "sha512-usY0HG5nyDUwtqpiZdETNbmKtw3QQ1jwYFZ9wi5iHzX2BcILwQKtYDJPo7XHTsu5Z0B2Hj3W9NNnbd+AjFWjqg==", + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/http-proxy-middleware/-/http-proxy-middleware-3.0.5.tgz", + "integrity": "sha512-GLZZm1X38BPY4lkXA01jhwxvDoOkkXqjgVyUzVxiEK4iuRu03PZoYHhHRwxnfhQMDuaxi3vVri0YgSro/1oWqg==", + "license": "MIT", "dependencies": { "@types/http-proxy": "^1.17.15", "debug": "^4.3.6", diff --git a/package.json b/package.json index fbb9093be..d424122eb 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "cryptpad", "description": "a collaborative office suite that is end-to-end encrypted and open-source", - "version": "2025.3.0", + "version": "2025.3.1", "license": "AGPL-3.0+", "repository": { "type": "git", @@ -20,12 +20,13 @@ "bootstrap-tokenfield": "^0.12.0", "chainpad": "^5.2.6", "chainpad-crypto": "^0.3.0", - "chainpad-listmap": "^1.1.1", - "chainpad-netflux": "^1.2.3", + "chainpad-listmap": "^1.2.0", + "chainpad-netflux": "^1.3.0", "chainpad-server": "^5.2.4", "ckeditor": "npm:ckeditor4@~4.22.1", "codemirror": "^5.19.0", "components-font-awesome": "^4.6.3", + "connect-gzip-static": "^4.2.1", "cookie-parser": "^1.4.7", "croppie": "^2.5.0", "dragula": "3.7.2", diff --git a/src/common/onlyoffice/current-version.js b/src/common/onlyoffice/current-version.js index 59c468850..ff70ffbad 100644 --- a/src/common/onlyoffice/current-version.js +++ b/src/common/onlyoffice/current-version.js @@ -4,8 +4,10 @@ (() => { const factory = () => { + const version = 8; return { - currentVersion: 'v7' + currentVersionNumber: version, + currentVersion: 'v' + version }; }; diff --git a/src/common/pad-types.js b/src/common/pad-types.js index 8c2f8014b..70756f552 100644 --- a/src/common/pad-types.js +++ b/src/common/pad-types.js @@ -27,7 +27,7 @@ const factory = (AppConfig = {}, ApiConfig = {}, setCustomize({AppConfig,ApiConfig}); } - const Types = { setCustomize }; + const Types = { OO_APPS, setCustomize }; Types.__defineGetter__("availableTypes", function () { if (ApiConfig.appsToDisable) { diff --git a/src/worker/async-store.js b/src/worker/async-store.js index 69362c97c..a65cd13b1 100644 --- a/src/worker/async-store.js +++ b/src/worker/async-store.js @@ -3,18 +3,21 @@ // SPDX-License-Identifier: AGPL-3.0-or-later (() => { -const factory = (ApiConfig = {}, Sortify, UserObject, ProxyManager, +const factory = (Sortify, UserObject, ProxyManager, Migrate, Hash, Util, Constants, Feedback, Realtime, Messaging, Pinpad, Rpc, Merge, Cache, SF, AccountTS, DriveTS, Cursor, Support, Integration, OnlyOffice, Mailbox, Profile, Team, Messenger, History, - Calendar, Block, NetConfig, AppConfig = {}, + Calendar, BadgeTS, Block, NetConfig, Crypto, ChainPad, CpNetflux, Listmap, Netflux, nThen) => { + let ApiConfig = {}; + let AppConfig = {}; const Account = AccountTS.Account; const Drive = DriveTS.Drive; + const Badge = BadgeTS.Badge; const window = globalThis; globalThis.nacl = globalThis.nacl || Crypto.Nacl; @@ -659,11 +662,15 @@ const factory = (ApiConfig = {}, Sortify, UserObject, ProxyManager, color: Store.getUserColor(), notifications: Util.find(proxy, ['mailboxes', 'notifications', 'channel']), curvePublic: proxy.curvePublic, + edPublic: proxy.edPublic, + netfluxId: store?.network?.webChannels?.[0]?.myID, + badge: Util.find(proxy, ['profile', 'badge']) }, // "priv" is not shared with other users but is needed by the apps priv: { clientId: clientId, edPublic: proxy.edPublic, + edPrivate: proxy.edPrivate, friends: proxy.friends || {}, settings: proxy.settings || NEW_USER_SETTINGS, thumbnails: disableThumbnails === false, @@ -2807,6 +2814,7 @@ const factory = (ApiConfig = {}, Sortify, UserObject, ProxyManager, loadUniversal(Integration, 'integration', waitFor); loadUniversal(Messenger, 'messenger', waitFor); loadUniversal(History, 'history', waitFor); + loadUniversal(Badge, 'badge', waitFor); loadOnlyOffice(); if (store) { store.messenger = store.modules['messenger']; @@ -2833,7 +2841,7 @@ const factory = (ApiConfig = {}, Sortify, UserObject, ProxyManager, // Call onCacheReady if the manager is not yet defined if (!manager) { - onCacheReady(clientId, waitFor()); + startCacheModules(clientId, returned, waitFor()); manager = store.manager; userObject = store.userObject; } @@ -3193,10 +3201,11 @@ const factory = (ApiConfig = {}, Sortify, UserObject, ProxyManager, andThen(); }; - const startCacheModules = (clientId, returned, cb) => { + const startCacheModules = (clientId, returned, _cb) => { + const cb = Util.mkAsync(_cb); onCacheReady(clientId, function () { - cb(returned); onCacheReadyEvt.fire(); + cb(returned); }); }; const startModules = (clientId, returned, cb) => { @@ -3231,6 +3240,10 @@ const factory = (ApiConfig = {}, Sortify, UserObject, ProxyManager, const requires = data.requires; + // data.noDrive indicates that we can use drive-less + // mode. It will be false is we are loading a safe hash + const requiresDrive = !data.noDrive; + // Mark initialized to true: new tabs will have to // wait for the entire worker to be ready initialized = true; @@ -3247,7 +3260,7 @@ const factory = (ApiConfig = {}, Sortify, UserObject, ProxyManager, // Now users will create a drive, but they may not // own an account - if (requires === 'pad') { + if (requires === 'pad' && !requiresDrive) { // Start with only the pad modules, callback // and then load the account and other modules return void onNoDrive(clientId, function (obj) { @@ -3270,7 +3283,7 @@ const factory = (ApiConfig = {}, Sortify, UserObject, ProxyManager, onPadRejectedEvt.reg(next); }); } - if (requires === 'file') { + if (requires === 'file' && !requiresDrive) { // Start with only the pad modules, callback // and then load the account and other modules return void onNoDrive(clientId, function (obj) { @@ -3455,7 +3468,6 @@ const factory = (ApiConfig = {}, Sortify, UserObject, ProxyManager, if (typeof(module) !== 'undefined' && module.exports) { // Code from customize can't be laoded directly in the build module.exports = factory( - undefined, require('json.sortify'), require('../common/user-object'), require('../common/proxy-manager'), @@ -3465,7 +3477,7 @@ if (typeof(module) !== 'undefined' && module.exports) { require('../common/common-constants'), require('../common/common-feedback'), require('../common/common-realtime'), - require('../common/common-messaging'), + require('./components/messaging'), require('../common/pinpad'), require('../common/rpc'), require('./components/merge-drive'), @@ -3483,9 +3495,9 @@ if (typeof(module) !== 'undefined' && module.exports) { require('./modules/messenger'), require('./modules/history'), require('./modules/calendar'), + require('./modules/badge'), require('../common/login-block'), require('../common/network-config'), - undefined, require('chainpad-crypto'), require('chainpad'), require('chainpad-netflux'), diff --git a/src/worker/components/mailbox-handlers.js b/src/worker/components/mailbox-handlers.js index 3f918e519..f5b00b386 100644 --- a/src/worker/components/mailbox-handlers.js +++ b/src/worker/components/mailbox-handlers.js @@ -985,7 +985,7 @@ const factory = (Messaging, Hash, Util, Crypto, Block) => { if (typeof(module) !== 'undefined' && module.exports) { module.exports = factory( - require('../../common/common-messaging'), + require('./messaging'), require('../../common/common-hash'), require('../../common/common-util'), require('chainpad-crypto'), @@ -993,7 +993,7 @@ if (typeof(module) !== 'undefined' && module.exports) { ); } else if ((typeof(define) !== 'undefined' && define !== null) && (define.amd !== null)) { define([ - '/common/common-messaging.js', + '/common/outer/messaging.js', '/common/common-hash.js', '/common/common-util.js', '/components/chainpad-crypto/crypto.js', diff --git a/src/common/common-messaging.js b/src/worker/components/messaging.js similarity index 95% rename from src/common/common-messaging.js rename to src/worker/components/messaging.js index f9b7fde24..a00419f24 100644 --- a/src/common/common-messaging.js +++ b/src/worker/components/messaging.js @@ -15,6 +15,7 @@ const factory = (Crypto, Hash, Util, Constants, Realtime) => { curvePublic: proxy.curvePublic, notifications: Util.find(proxy, ['mailboxes', 'notifications', 'channel']), avatar: proxy.profile && proxy.profile.avatar, + badge: proxy.profile && proxy.profile.badge, uid: proxy.uid, }; if (hash === false) { delete data.channel; } @@ -142,10 +143,10 @@ const factory = (Crypto, Hash, Util, Constants, Realtime) => { if (typeof(module) !== 'undefined' && module.exports) { module.exports = factory( require('chainpad-crypto'), - require('./common-hash'), - require('./common-util'), - require('./common-constants'), - require('./common-realtime') + require('../../common/common-hash'), + require('../../common/common-util'), + require('../../common/common-constants'), + require('../../common/common-realtime') ); } else if ((typeof(define) !== 'undefined' && define !== null) && (define.amd !== null)) { define([ diff --git a/src/worker/components/migrate-user-object.js b/src/worker/components/migrate-user-object.js index 638c062a6..23ef1103a 100644 --- a/src/worker/components/migrate-user-object.js +++ b/src/worker/components/migrate-user-object.js @@ -512,7 +512,7 @@ if (typeof(module) !== 'undefined' && module.exports) { require('../../common/common-feedback'), require('../../common/common-hash'), require('../../common/common-util'), - require('../../common/common-messaging'), + require('./messaging'), require('../../common/cryptget'), require('../modules/mailbox'), undefined, @@ -525,7 +525,7 @@ if (typeof(module) !== 'undefined' && module.exports) { '/common/common-feedback.js', '/common/common-hash.js', '/common/common-util.js', - '/common/common-messaging.js', + '/common/outer/messaging.js', '/common/cryptget.js', '/common/outer/mailbox.js', '/customize/messages.js', diff --git a/src/worker/modules/badge.ts b/src/worker/modules/badge.ts new file mode 100644 index 000000000..59c2d43dd --- /dev/null +++ b/src/worker/modules/badge.ts @@ -0,0 +1,114 @@ +import nacl from 'tweetnacl/nacl-fast'; +import nThen from 'nthen'; +import { Module, ModuleObject, Callback } from '../types' +import * as Util from '../../common/common-util.js'; + +export interface BadgeModule extends Module { + setCustomize: (data: any) => void +} + +let ApiConfig:any = {}; + +const checkApi = (ctx, ed, cb) => { + const badges = []; + const origin:string = ApiConfig?.httpUnsafeOrigin; + Util.fetchApi(origin, 'config', true, Config => { + const isAdmin = Config?.adminKeys?.includes(ed); + if (isAdmin) { badges.push('admin'); } + const isModerator = Config?.moderatorKeys?.includes(ed); + if (isModerator) { badges.push('moderator'); } + cb(badges); + }); +}; +const checkPremium = (ctx, ed, cb) => { + ctx.Store.anonRpcMsg('', { + msg: 'IS_PREMIUM', + data: ed + }, obj => { + let premium = Array.isArray(obj) && obj[0]; + cb(premium ? ['premium'] : []); + }); +}; + +const listBadges:Callback = (ctx, data, clientId, cb) => { + const badges = []; + const origin:string = ApiConfig?.httpUnsafeOrigin; + const ed = data.edPublic || ctx.store?.proxy?.edPublic; + nThen(waitFor => { + checkApi(ctx, ed, waitFor(list => { + Array.prototype.push.apply(badges, list); + })); + }).nThen(waitFor => { + checkPremium(ctx, ed, waitFor(list => { + Array.prototype.push.apply(badges, list); + })); + }).nThen(() => { + cb(badges); + }); +}; + +const allBadges = { + 'admin': checkApi, + 'moderator': checkApi, + 'premium': checkPremium +}; + +const checkBadge:Callback = (ctx, data, clientId, cb) => { + const { badge, ed, sig, nid/*, channel*/ } = data; + + const publicKey = Util.decodeBase64(ed); // Public key uint8 + const signature = Util.decodeBase64(sig); // badge uint8 + + // Check signature + const msg = nacl.sign.open(signature, publicKey); + if (!msg) { return void cb({verified: false}); } + + // Check channel (replay attack) + const msgStr:string = Util.encodeUTF8(msg); + const expected:string = nid; // + channel; + if (msgStr !== expected) { return void cb({verified: false}); } + + let f = allBadges[badge]; + if (!f) { return void cb({verified: false, error: 'EINVAL'}); } + + f(ctx, ed, list => { + cb({ + verified: list.includes(badge), + badge: badge + }); + }); +}; + +const Badge: BadgeModule = { + + init: (config, cb, emit) => { + + const ctx:any = { + store: config.store, + Store: config.Store, + updateMetadata: config.updateMetadata + }; + + return { + removeClient: () => {}, + execCommand: (clientId, obj, cb) => { + console.log('Exex command', clientId, obj); + const cmd = obj.cmd; + const data = obj.data; + if (cmd === 'LIST_BADGES') { + return void listBadges(ctx, data, clientId, cb); + } + if (cmd === 'CHECK_BADGE') { + return void checkBadge(ctx, data, clientId, cb); + } + cb(); + }, + } + }, + setCustomize: data => { + ApiConfig = data?.ApiConfig; + } + +}; + +export { Badge } diff --git a/src/worker/modules/mailbox.js b/src/worker/modules/mailbox.js index a908886f4..48359a73e 100644 --- a/src/worker/modules/mailbox.js +++ b/src/worker/modules/mailbox.js @@ -679,7 +679,7 @@ if (typeof(module) !== 'undefined' && module.exports) { require('../../common/common-util'), require('../../common/common-hash'), require('../../common/common-realtime'), - require('../../common/common-messaging'), + require('../components/messaging'), require('../../common/notify'), require('../components/mailbox-handlers'), require('chainpad-netflux'), @@ -691,7 +691,7 @@ if (typeof(module) !== 'undefined' && module.exports) { '/common/common-util.js', '/common/common-hash.js', '/common/common-realtime.js', - '/common/common-messaging.js', + '/common/outer/messaging.js', '/common/notify.js', '/common/outer/mailbox-handlers.js', 'chainpad-netflux', diff --git a/src/worker/modules/messenger.js b/src/worker/modules/messenger.js index 30cf1908d..281fc13d5 100644 --- a/src/worker/modules/messenger.js +++ b/src/worker/modules/messenger.js @@ -1126,7 +1126,7 @@ if (typeof(module) !== 'undefined' && module.exports) { require('../../common/common-hash'), require('../../common/common-util'), require('../../common/common-realtime'), - require('../../common/common-messaging'), + require('../components/messaging'), require('../../common/common-constants'), undefined, require('../../common/pad-types'), @@ -1138,7 +1138,7 @@ if (typeof(module) !== 'undefined' && module.exports) { '/common/common-hash.js', '/common/common-util.js', '/common/common-realtime.js', - '/common/common-messaging.js', + '/common/outer/messaging.js', '/common/common-constants.js', '/customize/messages.js', '/common/pad-types.js', diff --git a/src/worker/modules/profile.js b/src/worker/modules/profile.js index 0a170fc11..abd9a4816 100644 --- a/src/worker/modules/profile.js +++ b/src/worker/modules/profile.js @@ -99,6 +99,12 @@ const factory = (Util, Hash, Constants, Realtime, ctx.emit('UPDATE', ctx.listmap.proxy, ctx.clients.filter(function (clientId) { return clientId !== cId; })); + if (key === 'badge') { + ctx.Store.set(null, { + key: ['profile', 'badge'], + value: value || undefined + }, () => {}); + } cb(ctx.listmap.proxy); }); }; @@ -113,6 +119,7 @@ const factory = (Util, Hash, Constants, Realtime, var store = cfg.store; if (!store.loggedIn || !store.proxy.edPublic) { return; } var ctx = { + Store: cfg.Store, store: store, pinPads: cfg.pinPads, updateMetadata: cfg.updateMetadata, diff --git a/src/worker/modules/team.js b/src/worker/modules/team.js index 8a3c240cb..cd67627b8 100644 --- a/src/worker/modules/team.js +++ b/src/worker/modules/team.js @@ -2253,7 +2253,7 @@ if (typeof(module) !== 'undefined' && module.exports) { require('../../common/user-object'), require('../components/sharedfolder'), require('../components/roster'), - require('../../common/common-messaging'), + require('../components/messaging'), require('../../common/common-feedback'), require('../components/invitation'), require('../../common/cryptget'), @@ -2278,7 +2278,7 @@ if (typeof(module) !== 'undefined' && module.exports) { '/common/user-object.js', '/common/outer/sharedfolder.js', '/common/outer/roster.js', - '/common/common-messaging.js', + '/common/outer/messaging.js', '/common/common-feedback.js', '/common/outer/invitation.js', '/common/cryptget.js', diff --git a/src/worker/modules/test.ts b/src/worker/modules/test.ts index 0c8dd54bc..3a5b68805 100644 --- a/src/worker/modules/test.ts +++ b/src/worker/modules/test.ts @@ -12,7 +12,7 @@ export interface TestModuleObject extends ModuleObject { const TestModule: Module = { - init: (config, cb) => { + init: (config, cb, emit) => { let myTest = {}; cb(); diff --git a/src/worker/store.ts b/src/worker/store.ts index 8ab95321a..7929454ac 100644 --- a/src/worker/store.ts +++ b/src/worker/store.ts @@ -15,7 +15,6 @@ import * as Util from '../common/common-util.js'; import * as Hash from '../common/common-hash.js'; import * as Feedback from '../common/common-feedback.js'; import * as Realtime from '../common/common-realtime.js'; -import * as Messaging from '../common/common-messaging.js'; import * as Constants from '../common/common-constants.js'; import * as Credential from '../common/common-credential.js'; import * as ProxyManager from '../common/proxy-manager.js'; @@ -36,6 +35,7 @@ import * as AsyncConnector from './core/async-connector.js'; // Components import * as Migrate from './components/migrate-user-object.js'; +import * as Messaging from './components/messaging.js'; // Modules import * as Mailbox from './modules/mailbox.js'; @@ -48,6 +48,7 @@ import * as Team from './modules/team.js'; import * as Messenger from './modules/messenger.js'; import * as History from './modules/history.js'; import * as Calendar from './modules/calendar.js'; +import { Badge } from './modules/badge'; declare var WorkerGlobalScope: any declare var SharedWorkerGlobalScope: any @@ -76,7 +77,8 @@ let start = (cfg: StoreConfig):void => { Calendar, Store, Account, - Mailbox + Mailbox, + Badge ].forEach(dep => { if (typeof(dep.setCustomize) === "function") { dep.setCustomize(cfg); diff --git a/src/worker/types.ts b/src/worker/types.ts index a6d54687c..263575c45 100644 --- a/src/worker/types.ts +++ b/src/worker/types.ts @@ -7,7 +7,11 @@ export type Callback = (...args: any[]) => void export type ModuleConfig = { - emit: Function + store: any, + Store: any, + updateMetadata: Callback, + pinPads: Callback, + unpinPads: Callback } export type CommandData = { @@ -21,7 +25,7 @@ export interface ModuleObject { } export interface Module { - init: (config: ModuleConfig, cb: Callback) => T + init: (config: ModuleConfig, cb: Callback, emit: Callback) => T } diff --git a/www/common/common-messaging.js b/www/common/common-messaging.js deleted file mode 100644 index f9b7fde24..000000000 --- a/www/common/common-messaging.js +++ /dev/null @@ -1,161 +0,0 @@ -// SPDX-FileCopyrightText: 2023 XWiki CryptPad Team and contributors -// -// SPDX-License-Identifier: AGPL-3.0-or-later - -(() => { -const factory = (Crypto, Hash, Util, Constants, Realtime) => { - var Msg = {}; - - var createData = Msg.createData = function (proxy, hash) { - var data = { - channel: hash || Hash.createChannelId(), - displayName: proxy['cryptpad.username'], - profile: proxy.profile && proxy.profile.view, - edPublic: proxy.edPublic, - curvePublic: proxy.curvePublic, - notifications: Util.find(proxy, ['mailboxes', 'notifications', 'channel']), - avatar: proxy.profile && proxy.profile.avatar, - uid: proxy.uid, - }; - if (hash === false) { delete data.channel; } - return data; - }; - - var getFriend = Msg.getFriend = function (proxy, pubkey) { - if (!pubkey) { return; } - if (pubkey === proxy.curvePublic) { - var data = createData(proxy); - delete data.channel; - return data; - } - return proxy.friends ? proxy.friends[pubkey] : undefined; - }; - - var getFriendList = Msg.getFriendList = function (proxy) { - if (!proxy.friends) { proxy.friends = {}; } - return proxy.friends; - }; - - var eachFriend = function (friends, cb) { - Object.keys(friends).forEach(function (id) { - if (id === 'me') { return; } - cb(friends[id], id, friends); - }); - }; - - Msg.getFriendChannelsList = function (proxy) { - var list = []; - eachFriend(proxy.friends, function (friend) { - list.push(friend.channel); - }); - return list; - }; - - Msg.declineFriendRequest = function (store, data, cb) { - store.mailbox.sendTo('DECLINE_FRIEND_REQUEST', {}, { - channel: data.notifications, - curvePublic: data.curvePublic - }, function (obj) { - cb(obj); - }); - }; - Msg.acceptFriendRequest = function (store, data, cb) { - var friend = getFriend(store.proxy, data.curvePublic) || {}; - var myData = createData(store.proxy, friend.channel || data.channel); - store.mailbox.sendTo('ACCEPT_FRIEND_REQUEST', { user: myData }, { - channel: data.notifications, - curvePublic: data.curvePublic - }, function (obj) { - cb(obj); - }); - }; - Msg.addToFriendList = function (cfg, data, cb) { - var proxy = cfg.proxy; - var friends = getFriendList(proxy); - var pubKey = data.curvePublic; // todo validata data - - if (pubKey === proxy.curvePublic) { return void cb("E_MYKEY"); } - - friends[pubKey] = data; - - Realtime.whenRealtimeSyncs(cfg.realtime, function () { - cb(); - cfg.pinPads([data.channel], function (res) { - if (res.error) { console.error(res.error); } - }); - }); - }; - - Msg.updateMyData = function (store, curve) { - var myData = createData(store.proxy, false); - if (store.proxy.friends) { - store.proxy.friends.me = Util.clone(myData); - delete store.proxy.friends.me.channel; - } - if (store.modules['team']) { - store.modules['team'].updateMyData(myData); - } - var todo = function (friend) { - if (!friend || !friend.notifications) { return; } - delete friend.user; - myData.channel = friend.channel; - store.mailbox.sendTo('UPDATE_DATA', myData, { - channel: friend.notifications, - curvePublic: friend.curvePublic - }, function (obj) { - if (obj && obj.error) { console.error(obj); } - }); - }; - if (curve) { - var friend = getFriend(store.proxy, curve); - return void todo(friend); - } - eachFriend(store.proxy.friends || {}, todo); - }; - - Msg.removeFriend = function (store, curvePublic, cb) { - var proxy = store.proxy; - var friend = proxy.friends[curvePublic]; - if (!friend) { return void cb({error: 'ENOENT'}); } - if (!friend.notifications) { return void cb({error: 'EINVAL'}); } - - store.mailbox.sendTo('UNFRIEND', { - curvePublic: proxy.curvePublic - }, { - channel: friend.notifications, - curvePublic: friend.curvePublic - }, function (obj) { - if (obj && obj.error) { - return void cb(obj); - } - store.messenger.onFriendRemoved(curvePublic, friend.channel); - delete proxy.friends[curvePublic]; - Realtime.whenRealtimeSyncs(store.realtime, function () { - cb(obj); - }); - }); - }; - - return Msg; -}; - -if (typeof(module) !== 'undefined' && module.exports) { - module.exports = factory( - require('chainpad-crypto'), - require('./common-hash'), - require('./common-util'), - require('./common-constants'), - require('./common-realtime') - ); -} else if ((typeof(define) !== 'undefined' && define !== null) && (define.amd !== null)) { - define([ - '/components/chainpad-crypto/crypto.js', - '/common/common-hash.js', - '/common/common-util.js', - '/common/common-constants.js', - '/common/common-realtime.js', - ], factory); -} else { - // unsupported initialization -} -})(); diff --git a/www/common/cryptpad-common.js b/www/common/cryptpad-common.js index 816689160..d6a52bdbb 100644 --- a/www/common/cryptpad-common.js +++ b/www/common/cryptpad-common.js @@ -9,7 +9,6 @@ define([ '/common/common-util.js', '/common/common-hash.js', '/common/outer/cache-store.js', - '/common/common-messaging.js', '/common/common-constants.js', '/common/common-feedback.js', '/common/visible.js', @@ -20,13 +19,14 @@ define([ '/common/common-credential.js', '/customize/login.js', '/common/store-interface.js', + '/common/pad-types.js', '/customize/application_config.js', '/components/nthen/index.js', '/components/tweetnacl/nacl-fast.min.js' ], function (Config, Broadcast, Messages, Util, Hash, Cache, - Messaging, Constants, Feedback, Visible, UserObject, LocalStore, Channel, Block, - Cred, Login, Store, AppConfig, nThen) { + Constants, Feedback, Visible, UserObject, LocalStore, Channel, Block, + Cred, Login, Store, Types, AppConfig, nThen) { /* This file exposes functionality which is specific to Cryptpad, but not to any particular pad type. This includes functions for committing metadata @@ -1606,7 +1606,7 @@ define([ oldChannel = oldSecret.channel; var src = fileHost + Hash.getBlobPathFromHex(oldChannel); var key = oldSecret.keys && oldSecret.keys.cryptKey; - var cryptKey = window.Util.encodeBase64(key); + var cryptKey = Util.encodeBase64(key); var mt = document.createElement('media-tag'); mt.setAttribute('src', src); @@ -1690,7 +1690,7 @@ define([ if (!href) { return void cb({ error: 'EINVAL_HREF' }); } var parsed = Hash.parsePadUrl(href); if (!parsed.hash) { return void cb({ error: 'EINVAL_HREF' }); } - if (parsed.type !== 'sheet') { return void cb({ error: 'EINVAL_TYPE' }); } + if (!Types.OO_APPS.includes(parsed.type)) { return void cb({ error: 'EINVAL_TYPE' }); } var warning = false; var newHash, newRoHref; @@ -2468,9 +2468,9 @@ define([ }; common.getAnonymousKeys = function (formSeed, channel) { - var array = window.Util.decodeBase64(formSeed + channel); + var array = Util.decodeBase64(formSeed + channel); var hash = window.nacl.hash(array); - var secretKey = window.Util.encodeBase64(hash.subarray(32)); + var secretKey = Util.encodeBase64(hash.subarray(32)); var publicKey = Hash.getCurvePublicFromPrivate(secretKey); return { curvePrivate: secretKey, diff --git a/www/common/inner/access.js b/www/common/inner/access.js index 389311642..ee260137b 100644 --- a/www/common/inner/access.js +++ b/www/common/inner/access.js @@ -8,11 +8,12 @@ define([ '/common/common-hash.js', '/common/common-interface.js', '/common/common-ui-elements.js', + '/common/pad-types.js', '/common/inner/common-modal.js', '/common/hyperscript.js', '/customize/messages.js', '/components/nthen/index.js', -], function ($, Util, Hash, UI, UIElements, Modal, h, +], function ($, Util, Hash, UI, UIElements, Types, Modal, h, Messages, nThen) { var Access = {}; @@ -908,7 +909,7 @@ define([ // We should know it because the pad is stored, but it's better to check... //if (!data.noEditPassword && !opts.noEditPassword && owned && data.href) { if (!data.noEditPassword && !opts.noEditPassword && owned && data.href && parsed.type !== "form") { // TODO password change in forms block responses (validation & decryption) - var isOO = parsed.type === 'sheet'; + var isOO = Types?.OO_APPS?.includes(parsed.type); var isFile = parsed.hashData.type === 'file'; var isSharedFolder = parsed.type === 'drive'; diff --git a/www/common/inner/badges.js b/www/common/inner/badges.js new file mode 100644 index 000000000..2030f2362 --- /dev/null +++ b/www/common/inner/badges.js @@ -0,0 +1,32 @@ +// SPDX-FileCopyrightText: 2023 XWiki CryptPad Team and contributors +// +// SPDX-License-Identifier: AGPL-3.0-or-later + +define([ + '/common/hyperscript.js', + '/customize/messages.js', +], function (h, Messages) { + var Badges = {}; + + const badges = { + admin: 'fa-star', + moderator: 'fa-life-ring', + premium: 'fa-check-circle', + error: 'fa-exclamation-circle' + }; + + Badges.render = id => { + let icon = badges[id]; + if (!icon) { return; } + let cls = icon.indexOf('cptools') === 0 ? 'cptools '+icon : 'fa '+icon; + if (id === 'error') { cls += ' cp-badge-error'; } + return h('i', { + 'class': `cp-badge ${cls}`, + 'title': Messages[`badges_${id}`] || id + }); + }; + + return Badges; +}); + + diff --git a/www/common/inner/common-mediatag.js b/www/common/inner/common-mediatag.js index 15a744a04..ea0f15545 100644 --- a/www/common/inner/common-mediatag.js +++ b/www/common/inner/common-mediatag.js @@ -186,7 +186,7 @@ define([ if (avatars[href]) { var nodes = $.parseHTML(avatars[href]); var $el = $(nodes[0]); - $container.append($el); + $container.empty().append($el); return void cb($el); } @@ -222,7 +222,7 @@ define([ if (typeof data !== "number") { return void displayDefault(); } if (Util.bytesToMegabytes(data) > 0.5) { return void displayDefault(); } var mt = UI.mediaTag(src, cryptKey); - var $img = $(mt).appendTo($container); + var $img = $(mt).appendTo($container.empty()); MT.displayMediatagImage(common, $img, function (err, $image) { if (err) { return void console.error(err); } centerImage($img, $image); diff --git a/www/common/onlyoffice/current-version.js b/www/common/onlyoffice/current-version.js index 59c468850..ff70ffbad 100644 --- a/www/common/onlyoffice/current-version.js +++ b/www/common/onlyoffice/current-version.js @@ -4,8 +4,10 @@ (() => { const factory = () => { + const version = 8; return { - currentVersion: 'v7' + currentVersionNumber: version, + currentVersion: 'v' + version }; }; diff --git a/www/common/onlyoffice/inner.js b/www/common/onlyoffice/inner.js index 9b346e5c3..d5c7d3332 100644 --- a/www/common/onlyoffice/inner.js +++ b/www/common/onlyoffice/inner.js @@ -16,16 +16,14 @@ define([ '/common/hyperscript.js', '/api/config', '/customize/messages.js', - '/customize/application_config.js', '/components/chainpad/chainpad.dist.js', '/file/file-crypto.js', '/common/onlyoffice/history.js', '/common/onlyoffice/oocell_base.js', '/common/onlyoffice/oodoc_base.js', '/common/onlyoffice/ooslide_base.js', - '/common/outer/worker-channel.js', - '/common/outer/x2t.js', + '/common/onlyoffice/current-version.js', '/components/file-saver/FileSaver.min.js', 'css!/components/bootstrap/dist/css/bootstrap.min.css', @@ -45,15 +43,13 @@ define([ h, ApiConfig, Messages, - AppConfig, ChainPad, FileCrypto, History, EmptyCell, EmptyDoc, EmptySlide, - Channel, - X2T) + OOCurrentVersion) { var saveAs = window.saveAs; var APP = window.APP = { @@ -64,9 +60,7 @@ define([ var CHECKPOINT_INTERVAL = 100; var FORCE_CHECKPOINT_INTERVAL = 10000; var DISPLAY_RESTORE_BUTTON = false; - var NEW_VERSION = 7; // version of the .bin, patches and ChainPad formats var PENDING_TIMEOUT = 30000; - var CURRENT_VERSION = X2T.CURRENT_VERSION; const HISTORY_KEEPER_INDEX_USER = 1; const READ_ONLY_INDEX_USER = 2; @@ -99,20 +93,19 @@ define([ hashes: {}, ids: {}, mediasSources: {}, - version: privateData.ooForceVersion ? Number(privateData.ooForceVersion) : NEW_VERSION + version: privateData.ooForceVersion ? Number(privateData.ooForceVersion) : OOCurrentVersion.currentVersionNumber }; var oldHashes = {}; var oldIds = {}; var oldLocks = {}; var myUniqueOOId; var myOOId; - var sessionId = Hash.createChannelId(); var cpNfInner; let integrationChannel; var evOnPatch = Util.mkEvent(); var evOnSync = Util.mkEvent(); - var evIntegrationSave = Util.mkEvent(); + const evIntegrationSave = Util.mkEvent(); // This structure is used for caching media data and blob urls for each media cryptpad url var mediasData = {}; @@ -411,9 +404,9 @@ define([ _content.hashes[1] = { file: data.url, index: 0, - version: NEW_VERSION + version: OOCurrentVersion.currentVersionNumber }; - _content.version = NEW_VERSION; + _content.version = OOCurrentVersion.currentVersionNumber; _content.channel = Hash.createChannelId(); _content.ids = {}; sframeChan.query('Q_SAVE_AS_TEMPLATE', { @@ -467,7 +460,7 @@ define([ file: data.url, hash: ev.hash, index: ev.index, - version: NEW_VERSION + version: OOCurrentVersion.currentVersionNumber }; oldHashes = JSON.parse(JSON.stringify(content.hashes)); content.locks = {}; @@ -475,7 +468,7 @@ define([ // If this is a migration, set the new version if (APP.migrate) { delete content.migration; - content.version = NEW_VERSION; + content.version = OOCurrentVersion.currentVersionNumber; } APP.onLocal(); APP.realtime.onSettle(function () { @@ -1072,10 +1065,8 @@ define([ } }; - var handleAuth = function (obj, send) { - //setEditable(false); - - var changes = []; + const getInitialChanges = function() { + const changes = []; if (content.version > 2) { ooChannel.queue.forEach(function (data) { Array.prototype.push.apply(changes, data.msg.changes); @@ -1085,55 +1076,14 @@ define([ ooChannel.cpIndex += ooChannel.queue.length; var last = ooChannel.queue.pop(); if (last) { ooChannel.lastHash = last.hash; } - } else { + } + return changes; + }; + + const onAuth = function () { + if (content.version <= 2) { setEditable(false, true); } - send({ - type: "authChanges", - changes: changes - }); - - // Answer to the auth command - var p = getParticipants(); - send({ - type: "auth", - result: 1, - sessionId: sessionId, - participants: p.list, - locks: [], - changes: [], - changesIndex: 0, - indexUser: p.index, - buildVersion: "5.2.6", - buildNumber: 2, - licenseType: 3, - //"g_cAscSpellCheckUrl": "/spellchecker", - //"settings":{"spellcheckerUrl":"/spellchecker","reconnection":{"attempts":50,"delay":2000}} - }); - // Open the document - send({ - type: "documentOpen", - data: {"type":"open","status":"ok","data":{"Editor.bin":obj.openCmd.url}} - }); - - /* - // TODO: make sure we don't have new popups that can break our integration - var observer = new MutationObserver(function(mutations) { - mutations.forEach(function(mutation) { - if (mutation.type === "childList") { - for (var i = 0; i < mutation.addedNodes.length; i++) { - if (mutation.addedNodes[i].classList.contains('asc-window') && - mutation.addedNodes[i].classList.contains('alert')) { - $(mutation.addedNodes[i]).find('button').not('.custom').click(); - } - } - } - }); - }); - observer.observe(window.frames[0].document.body, { - childList: true, - }); - */ }; var handleLock = function (obj, send) { @@ -1405,224 +1355,205 @@ define([ }); }; + const send = ooChannel.send = function (obj, force) { + // can't push to OO before reloading cp + if (APP.onStrictSaveChanges && !force) { return; } + // We only need to release locks for sheets + const type = common.getMetadataMgr().getPrivateData().ooType; + if (type !== "sheet" && obj.type === "releaseLock") { return; } + if (type === "presentation" && obj.type === "cp_theme") { + console.error(obj); + return; + } - var makeChannel = function () { - var msgEv = Util.mkEvent(); - var iframe = $('#cp-app-oo-editor > iframe')[0].contentWindow; - var type = common.getMetadataMgr().getPrivateData().ooType; - window.addEventListener('message', function (msg) { - if (msg.source !== iframe) { return; } - msgEv.fire(msg); - }); - var postMsg = function (data) { - iframe.postMessage(data, ApiConfig.httpSafeOrigin); - }; - Channel.create(msgEv, postMsg, function (chan) { - APP.chan = chan; + debug(obj, 'toOOClient'); + APP.docEditor.sendMessageToOO(obj); + if (obj && obj.type === "saveChanges") { + evIntegrationSave.fire(); + } + }; - var send = ooChannel.send = function (obj, force) { - // can't push to OO before reloading cp - if (APP.onStrictSaveChanges && !force) { return; } - // We only need to release locks for sheets - if (type !== "sheet" && obj.type === "releaseLock") { return; } - if (type === "presentation" && obj.type === "cp_theme") { + const fromOOHandler = function (obj) { + debug(obj, 'fromOOClient'); + switch (obj.type) { + case "auth": + // Handled by onlyoffice-editor now + break; + case "isSaveLock": + // TODO ping the server to check if we're online first? + if (!offline) { + if (APP.waitLock) { + APP.waitLock.reg(function () { + send({ + type: "saveLock", + saveLock: false + }, true); + }); + } else { + send({ + type: "saveLock", + saveLock: false + }, true); + } + } + break; + case "cursor": + if (cursor && cursor.updateCursor) { + cursor.updateCursor({ + type: "cursor", + messages: [{ + cursor: obj.cursor, + time: +new Date(), + user: myUniqueOOId, + useridoriginal: myOOId + }] + }); + } + break; + case "forceSaveStart": + if (APP.integrationSave) { + APP.integrationSave(obj => { + if (obj?.error) { + console.error(obj.error); + return void UI.warn(Messages.error); + } + content.integrationSave = `${myUniqueOOId}-${+new Date()}`; + APP.integrationSaved = content.integrationSave; + APP.onLocal(); + UI.log(Messages.saved); + }); + } + break; + case "getLock": + handleLock(obj, send); + break; + case "getMessages": + // OO chat messages? + send({ type: "message" }); + break; + case "saveChanges": + // If we have unsaved data before reloading for a checkpoint... + if (APP.onStrictSaveChanges) { + const type = common.getMetadataMgr().getPrivateData().ooType; + delete APP.unsavedLocks; + APP.unsavedChanges = { + type: "saveChanges", + changes: parseChanges(obj.changes), + changesIndex: ooChannel.cpIndex || 0, + locks: type === "sheet" ? [] : APP.unsavedLocks, + excelAdditionalInfo: null, + recover: true + }; + APP.onStrictSaveChanges(); + return; + } + var AscCommon = window.frames[0] && window.frames[0].AscCommon; + if (Util.find(AscCommon, ['CollaborativeEditing','m_bFast']) + && APP.themeLocked) { + obj = APP.themeLocked; + APP.themeLocked = undefined; + obj.type = "cp_theme"; console.error(obj); + } + if (APP.themeRemote) { + delete APP.themeRemote; + send({ + type: "unSaveLock", + index: ooChannel.cpIndex, + time: +new Date() + }); return; } - debug(obj, 'toOO'); - chan.event('CMD', obj); - if (obj && obj.type === "saveChanges") { - evIntegrationSave.fire(); - } - }; - - chan.on('CMD', function (obj) { - debug(obj, 'fromOO'); - switch (obj.type) { - case "auth": - handleAuth(obj, send); - break; - case "isSaveLock": - // TODO ping the server to check if we're online first? - if (!offline) { - if (APP.waitLock) { - APP.waitLock.reg(function () { - send({ - type: "saveLock", - saveLock: false - }, true); - }); - } else { - send({ - type: "saveLock", - saveLock: false - }, true); + // We're sending our changes to netflux + handleChanges(obj, send); + // If we're alone, clean up the medias + var m = metadataMgr.getChannelMembers().slice().filter(function (nId) { + return nId.length === 32; + }); + if (m.length === 1 && APP.loadingImage <= 0) { + try { + // "docs" contains the correct images that we've just uploaded + // "docs2" contains the correct images from the .bin checkpoint + // both of them are not reliable in the other case + var docs = getWindow().AscCommon.g_oDocumentUrls.urls; + var docs2 = getEditor().ImageLoader.map_image_index; + var mediasSources = getMediasSources(); + Object.keys(mediasSources).forEach(function (name) { + if (!docs && !docs2) { return; } + if (!docs['media/'+name] && !docs2[name]) { + delete mediasSources[name]; } - } - break; - case "cursor": - if (cursor && cursor.updateCursor) { - cursor.updateCursor({ - type: "cursor", - messages: [{ - cursor: obj.cursor, - time: +new Date(), - user: myUniqueOOId, - useridoriginal: myOOId - }] - }); - } - break; - case "forceSaveStart": - if (APP.integrationSave) { - APP.integrationSave(obj => { - if (obj?.error) { - console.error(obj.error); - return void UI.warn(Messages.error); - } - content.integrationSave = `${myUniqueOOId}-${+new Date()}`; - APP.integrationSaved = content.integrationSave; - APP.onLocal(); - UI.log(Messages.saved); - }); - } - break; - case "getLock": - handleLock(obj, send); - break; - case "getMessages": - // OO chat messages? - send({ type: "message" }); - break; - case "saveChanges": - if (readOnly) { - return; - } - - // If we have unsaved data before reloading for a checkpoint... - if (APP.onStrictSaveChanges) { - delete APP.unsavedLocks; - APP.unsavedChanges = { - type: "saveChanges", - changes: parseChanges(obj.changes), - changesIndex: ooChannel.cpIndex || 0, - locks: type === "sheet" ? [] : APP.unsavedLocks, - excelAdditionalInfo: null, - recover: true - }; - APP.onStrictSaveChanges(); - return; - } - var AscCommon = window.frames[0] && window.frames[0].AscCommon; - if (Util.find(AscCommon, ['CollaborativeEditing','m_bFast']) - && APP.themeLocked) { - obj = APP.themeLocked; - APP.themeLocked = undefined; - obj.type = "cp_theme"; - console.error(obj); - } - if (APP.themeRemote) { - delete APP.themeRemote; - send({ - type: "unSaveLock", - index: ooChannel.cpIndex, - time: +new Date() - }); - return; - } - - // We're sending our changes to netflux - handleChanges(obj, send); - // If we're alone, clean up the medias - var m = metadataMgr.getChannelMembers().slice().filter(function (nId) { - return nId.length === 32; }); - if (m.length === 1 && APP.loadingImage <= 0) { - try { - // "docs" contains the correct images that we've just uploaded - // "docs2" contains the correct images from the .bin checkpoint - // both of them are not reliable in the other case - var docs = getWindow().AscCommon.g_oDocumentUrls.urls; - var docs2 = getEditor().ImageLoader.map_image_index; - var mediasSources = getMediasSources(); - Object.keys(mediasSources).forEach(function (name) { - if (!docs && !docs2) { return; } - if (!docs['media/'+name] && !docs2[name]) { - delete mediasSources[name]; - } - }); - APP.onLocal(); - } catch (e) {} - } - break; - case "unLockDocument": - if (obj.releaseLocks && content.locks && content.locks[getId()]) { - send({ - type: "releaseLock", - locks: getUserLock(getId()) - }); - delete content.locks[getId()]; - APP.onLocal(); - } - if (obj.isSave) { - send({ - type: "unSaveLock", - time: -1, - index: -1 - }); - } - if (APP.onDocumentUnlock) { - APP.onDocumentUnlock(); - APP.onDocumentUnlock = undefined; - } - break; - case 'openDocument': - // When duplicating a slide, OO may ask the URLs of the images - // in that slide - var _obj = obj.message; - if (_obj.c === "imgurls") { - var _mediasSources = getMediasSources(); - var images = _obj.data || []; - if (!Array.isArray(images)) { return; } - var urls = []; - nThen(function (waitFor) { - images.forEach(function (name) { - if (/^data\:image/.test(name)) { - Util.fetch(name, waitFor(function (err, u8) { - if (err) { return; } - var b = new Blob([u8]); - urls.push(URL.createObjectURL(b)); - })); - return; - } - var data = _mediasSources[name]; - if (!data) { return; } - var media = mediasData[data.src]; - if (!media) { return; } - urls.push({ - path: name, - url: media.blobUrl, - }); - }); - }).nThen(function () { - send({ - type: "documentOpen", - data: { - type: "imgurls", - status: "ok", - data: { - urls: urls, - error: 0 - } - } - }); - }); - } - break; + APP.onLocal(); + } catch (e) {} } - }); - }); + break; + case "unLockDocument": + if (obj.releaseLocks && content.locks && content.locks[getId()]) { + send({ + type: "releaseLock", + locks: getUserLock(getId()) + }); + delete content.locks[getId()]; + APP.onLocal(); + } + if (obj.isSave) { + send({ + type: "unSaveLock", + time: -1, + index: -1 + }); + } + if (APP.onDocumentUnlock) { + APP.onDocumentUnlock(); + APP.onDocumentUnlock = undefined; + } + break; + case 'openDocument': + // When duplicating a slide, OO may ask the URLs of the images + // in that slide + var _obj = obj.message; + if (_obj.c === "imgurls") { + var _mediasSources = getMediasSources(); + var images = _obj.data || []; + if (!Array.isArray(images)) { return; } + var urls = []; + nThen(function (waitFor) { + images.forEach(function (name) { + if (/^data\:image/.test(name)) { + Util.fetch(name, waitFor(function (err, u8) { + if (err) { return; } + var b = new Blob([u8]); + urls.push(URL.createObjectURL(b)); + })); + return; + } + var data = _mediasSources[name]; + if (!data) { return; } + var media = mediasData[data.src]; + if (!media) { return; } + urls.push({ + path: name, + url: media.blobUrl, + }); + }); + }).nThen(function () { + send({ + type: "documentOpen", + data: { + type: "imgurls", + status: "ok", + data: { + urls: urls, + error: 0 + } + } + }); + }); + } + break; + } }; var x2tConvertData = function (data, fileName, format, cb) { @@ -1680,22 +1611,231 @@ define([ // to be downloaded and decrypted before converting to xlsx var downloadImages = {}; - var firstOO = true; - startOO = function (blob, file, force) { - if (APP.ooconfig && !force) { return void console.error('already started'); } - var url = URL.createObjectURL(blob); - var lock = !APP.history && (APP.migrate); + const onAppReady = function() { + APP.docEditor.getIframe().setAttribute('tabindex', '-1'); + var css = // Old OO + //'#id-toolbar-full .toolbar-group:nth-child(2), #id-toolbar-full .separator:nth-child(3) { display: none; }' + + //'#fm-btn-save { display: none !important; }' + + //'#panel-settings-general tr.autosave { display: none !important; }' + + //'#panel-settings-general tr.coauth { display: none !important; }' + + //'#header { display: none !important; }' + + '#title-doc-name { display: none !important; }' + + '#title-user-name { display: none !important; }' + + (supportsXLSX() ? '' : '#slot-btn-dt-print { display: none !important; }') + + // New OO: + 'section[data-tab="ins"] .separator:nth-last-child(2) { display: none !important; }' + // separator + '#slot-btn-insequation { display: none !important; }' + // Insert equation + //'#asc-gen125 { display: none !important; }' + // Disable presenter mode + //'.toolbar .tabs .ribtab:not(.canedit) { display: none !important; }' + // Switch collaborative mode + '#fm-btn-info { display: none !important; }' + // Author name, doc title, etc. in "File" (menu entry) + '#panel-info { display: none !important; }' + // Same but content + '#image-button-from-url { display: none !important; }' + // Inline image settings: replace with url + '.cp-from-url, #textart-button-from-url { display: none !important; }' + // Spellcheck language + '.statusbar .cnt-lang { display: none !important; }' + // Spellcheck language + '.statusbar #btn-doc-spell { display: none !important; }' + // Spellcheck button + '#file-menu-panel .devider { display: none !important; }' + // separator in the "File" menu + '#left-btn-spellcheck, #left-btn-about { display: none !important; }'+ + 'div.btn-users.dropdown-toggle { display: none; !important }'; + if (readOnly) { + css += '#toolbar { display: none !important; }'; + //css += '#app-title { display: none !important; }'; // OnlyOffice logo + doc title + //css += '#file-menu-panel { top: 28px !important; }'; // Position of the "File" menu + } + APP.docEditor.injectCSS(css); + setTimeout(function () { + $(window).trigger('resize'); + }); + if (UI.findOKButton().length) { + UI.findOKButton().on('focusout', function () { + window.setTimeout(function () { UI.findOKButton().focus(); }); + }); + } + }; - var fromContent = metadataMgr.getPrivateData().fromContent; - if (!firstOO) { fromContent = undefined; } - firstOO = false; + const onError = function() { + console.error(arguments); + if (APP.isDownload) { + var sframeChan = common.getSframeChannel(); + sframeChan.event('EV_OOIFRAME_DONE', ''); + } + }; - // Starting from version 3, we can use the view mode again - // defined but never used - //var mode = (content && content.version > 2 && lock) ? "view" : "edit"; + const onDocumentReady = function(lock, lang, fromContent, file, force) { + evOnSync.fire(); + var onMigrateRdy = Util.mkEvent(); + onMigrateRdy.reg(function () { + var div = h('div.cp-oo-x2tXls', [ + h('span.fa.fa-spin.fa-spinner'), + h('span', Messages.oo_sheetMigration_loading) + ]); + APP.migrateModal = UI.openCustomModal(UI.dialog.customModal(div, {buttons: []})); + makeCheckpoint(true); + }); + // DEPRECATED: from version 3, the queue is sent again during init + if (APP.migrate && ((content.version || 1) <= 2)) { + // The doc is ready, fix the worksheets IDs and push the queue + fixSheets(); + // Push changes since last cp + ooChannel.ready = true; + var changes = []; + var changesIndex; + ooChannel.queue.forEach(function (data) { + Array.prototype.push.apply(changes, data.msg.changes); + changesIndex = data.msg.changesIndex; + //ooChannel.send(data.msg); + }); + ooChannel.cpIndex += ooChannel.queue.length; + var last = ooChannel.queue.pop(); + if (last) { ooChannel.lastHash = last.hash; } - var lang = (window.cryptpadLanguage || navigator.language || navigator.userLanguage || '').slice(0,2); + var onDocUnlock = function () { + // Migration required but read-only: continue... + if (readOnly) { + setEditable(true); + try { getEditor().asc_setRestriction(true); } catch (e) {} + } else { + // No changes after the cp: migrate now + onMigrateRdy.fire(); + } + }; + // Send the changes all at once + if (changes.length) { + setTimeout(function () { + ooChannel.send({ + type: 'saveChanges', + changesIndex: changesIndex, + changes: changes, + locks: [] + }); + APP.onDocumentUnlock = onDocUnlock; + }, 5000); + return; + } + onDocUnlock(); + return; + } + + if (lock || readOnly) { + try { getEditor().asc_setRestriction(true); } catch (e) {} + //getEditor().setViewModeDisconnect(); // can't be used anymore, display an OO error popup + } else { + setEditable(true); + deleteOfflineLocks(); + handleNewLocks({}, content.locks); + if (APP.unsavedChanges) { + var unsaved = APP.unsavedChanges; + delete APP.unsavedChanges; + rtChannel.sendMsg(unsaved, null, function (err, hash) { + if (err) { return void UI.alert(Messages.oo_lostEdits); } + // This is supposed to be a "send" function to tell our OO + // to unlock the cell. We use this to know that the patch was + // correctly sent so that we can apply it to our OO too. + ooChannel.send(unsaved); + ooChannel.cpIndex++; + ooChannel.lastHash = hash; + }); + } + + if (APP.startNew) { + var w = getWindow(); + if (lang === "fr") { lang = 'fr-fr'; } + var l = w.Common.util.LanguageInfo.getLocalLanguageCode(lang); + getEditor().asc_setDefaultLanguage(l); + } + + if (APP.oldCursor) { + var app = common.getMetadataMgr().getPrivateData().ooType; + var d; + if (app === 'doc') { + d = getEditor().GetDocument().Document; + } else if (app === 'presentation') { + d = getEditor().GetPresentation().Presentation; + } + if (d) { + d.SetSelectionState(APP.oldCursor); + d.UpdateSelection(); + } + delete APP.oldCursor; + } + if (integrationChannel) { + APP.onDocumentUnlock = () => { + integrationChannel.event('EV_INTEGRATION_READY'); + }; + } + } + delete APP.startNew; + + if (fromContent && !lock && Array.isArray(fromContent.content)) { + makePatch(fromContent.content); + } + + if (APP.isDownload) { + delete APP.isDownload; + var bin = getContent(); + if (!supportsXLSX()) { + return void sframeChan.event('EV_OOIFRAME_DONE', bin, {raw: true}); + } + nThen(function (waitFor) { + // wait for all the images to be loaded before converting + Object.keys(downloadImages).forEach(function (name) { + downloadImages[name].reg(waitFor()); + }); + }).nThen(function () { + x2tConvertData(bin, 'filename.bin', file.type, function (xlsData) { + sframeChan.event('EV_OOIFRAME_DONE', xlsData, {raw: true}); + }); + }); + return; + } + + + if (isLockedModal.modal && force) { + isLockedModal.modal.closeModal(); + delete isLockedModal.modal; + if (!APP.history) { + $('#cp-app-oo-editor > iframe')[0].contentWindow.focus(); + } + } + + if (APP.template) { + try { getEditor().asc_setRestriction(true); } catch (e) {} + //getEditor().setViewModeDisconnect(); + UI.removeLoadingScreen(); + makeCheckpoint(true); + return; + } + + APP.onLocal(); // Add our data to the userlist + + if (APP.history) { + try { + getEditor().asc_setRestriction(true); + } catch (e) {} + } + + if (lock && !readOnly) { // Lock = !history && migrate + onMigrateRdy.fire(); + } + + if (APP.initCheckpoint) { + getEditor().asc_setRestriction(true); + makeCheckpoint(true); + } + + // Check if history can/should be trimmed + var cp = getLastCp(); + if (cp && cp.file && cp.hash) { + var channels = [{ + channel: content.channel, + lastKnownHash: cp.hash + }]; + common.checkTrimHistory(channels); + } + }; + + const createOOConfig = function(blob, file, lock, fromContent, lang, force) { + const url = URL.createObjectURL(blob); let username = Util.find(privateData, ['integrationConfig', 'user', 'name']) || metadataMgr.getUserData().name || Messages.anonymous; @@ -1704,8 +1844,7 @@ define([ //let ec = integrationConfig?.editorConfig; let dc = integrationConfig?.document; - // Config - APP.ooconfig = { + const ooconfig = { document: { fileType: file.type, key: "fresh", @@ -1714,6 +1853,7 @@ define([ permissions: { download: dc?.permissions?.download || false, print: dc?.permissions?.print || true, + protect: false } }, "documentType": file.doc, @@ -1737,231 +1877,57 @@ define([ "lang": lang }, "events": { - "onAppReady": function(/*evt*/) { - var $iframe = $('iframe[name="frameEditor"]').contents(); - $iframe.prop('tabindex', '-1'); - var $tb = $iframe.find('head'); - var css = // Old OO - //'#id-toolbar-full .toolbar-group:nth-child(2), #id-toolbar-full .separator:nth-child(3) { display: none; }' + - //'#fm-btn-save { display: none !important; }' + - //'#panel-settings-general tr.autosave { display: none !important; }' + - //'#panel-settings-general tr.coauth { display: none !important; }' + - //'#header { display: none !important; }' + - '#title-doc-name { display: none !important; }' + - '#title-user-name { display: none !important; }' + - (supportsXLSX() ? '' : '#slot-btn-dt-print { display: none !important; }') + - // New OO: - 'section[data-tab="ins"] .separator:nth-last-child(2) { display: none !important; }' + // separator - '#slot-btn-insequation { display: none !important; }' + // Insert equation - //'#asc-gen125 { display: none !important; }' + // Disable presenter mode - //'.toolbar .tabs .ribtab:not(.canedit) { display: none !important; }' + // Switch collaborative mode - '#fm-btn-info { display: none !important; }' + // Author name, doc title, etc. in "File" (menu entry) - '#panel-info { display: none !important; }' + // Same but content - '#image-button-from-url { display: none !important; }' + // Inline image settings: replace with url - '.cp-from-url, #textart-button-from-url { display: none !important; }' + // Spellcheck language - '.statusbar .cnt-lang { display: none !important; }' + // Spellcheck language - '.statusbar #btn-doc-spell { display: none !important; }' + // Spellcheck button - '#file-menu-panel .devider { display: none !important; }' + // separator in the "File" menu - '#left-btn-spellcheck, #left-btn-about { display: none !important; }'+ - 'div.btn-users.dropdown-toggle { display: none; !important }'; - if (readOnly) { - css += '#toolbar { display: none !important; }'; - //css += '#app-title { display: none !important; }'; // OnlyOffice logo + doc title - //css += '#file-menu-panel { top: 28px !important; }'; // Position of the "File" menu - } - $('