diff --git a/.github/workflows/wopi.yml b/.github/workflows/wopi.yml new file mode 100644 index 0000000000..f84ea0856f --- /dev/null +++ b/.github/workflows/wopi.yml @@ -0,0 +1,114 @@ +# SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors +# SPDX-License-Identifier: MIT +name: WOPI validator tests + +on: + pull_request: + push: + branches: + - main + - stable* + +env: + APP_NAME: richdocuments + +jobs: + changes: + runs-on: ubuntu-latest + + outputs: + src: ${{ steps.changes.outputs.src}} + + steps: + - uses: dorny/paths-filter@4512585405083f25c027a35db413c2b3b9006d50 # v2.11.1 + id: changes + continue-on-error: true + with: + filters: | + src: + - '.github/workflows/**' + - 'appinfo/**' + - 'lib/**' + - 'templates/**' + - 'tests/**' + - 'vendor/**' + - 'vendor-bin/**' + - '.php-cs-fixer.dist.php' + - 'composer.json' + - 'composer.lock' + + sqlite: + runs-on: ubuntu-latest + + needs: changes + if: needs.changes.outputs.src != 'false' + + strategy: + # do not stop on another job's failure + fail-fast: false + matrix: + php-versions: ['8.1'] + server-versions: ['master'] + + name: wopi-${{ matrix.php-versions }}-${{ matrix.server-versions }} + + services: + collabora: + image: collabora/code:latest + env: + extra_params: '--o:ssl.enable=false' + aliasgroup1: 'http://nextcloud' + ports: + - "9980:9980" + + steps: + - name: Checkout server + uses: actions/checkout@v2 + with: + repository: nextcloud/server + ref: ${{ matrix.server-versions }} + + - name: Checkout submodules + shell: bash + run: | + auth_header="$(git config --local --get http.https://github.com/.extraheader)" + git submodule sync --recursive + git -c "http.extraheader=$auth_header" -c protocol.version=2 submodule update --init --force --recursive --depth=1 + + - name: Checkout app + uses: actions/checkout@v2 + with: + path: apps/${{ env.APP_NAME }} + + - name: Set up php ${{ matrix.php-versions }} + uses: shivammathur/setup-php@v2 + with: + php-version: ${{ matrix.php-versions }} + tools: phpunit:8.5.14 + extensions: mbstring, iconv, fileinfo, intl, sqlite, pdo_sqlite, gd, zip, apcu + ini-values: + apc.enable_cli=on + coverage: none + + - name: Set up Nextcloud + env: + DB_PORT: 4444 + run: | + mkdir data + echo '"\OC\Memcache\APCu","hashing_default_password"=>true];' > config/config.php + ./occ maintenance:install --verbose --database=sqlite --database-name=nextcloud --database-host=127.0.0.1 --database-port=$DB_PORT --database-user=root --database-pass=rootpassword --admin-user admin --admin-pass admin + ./occ config:system:set trusted_domains 1 --value=172.17.0.1 + ./occ app:enable --force ${{ env.APP_NAME }} + + # For now we require a some wopi server to be setup to generate a token + ./occ config:app:set richdocuments wopi_url --value="http://localhost:9980" + ./occ config:app:set richdocuments public_wopi_url --value="http://localhost:9980" + ./occ config:system:set allow_local_remote_servers --value true --type bool + ./occ richdocuments:activate-config + + + - name: Run WOPI validator tests + working-directory: apps/${{ env.APP_NAME }} + run: | + PHP_CLI_SERVER_WORKERS=10 php -S 172.17.0.1:8080 -t ../../ & + NEXTCLOUD_URL=http://172.17.0.1:8080 ./tests/wopi-test.sh diff --git a/composer.json b/composer.json index cee98582b2..0bd1f6bc99 100644 --- a/composer.json +++ b/composer.json @@ -13,7 +13,8 @@ }, "require": { "ext-json": "*", - "ext-simplexml": "*" + "ext-simplexml": "*", + "champs-libres/wopi-lib": "dev-master" }, "require-dev": { "roave/security-advisories": "dev-master", @@ -23,7 +24,8 @@ "nextcloud/coding-standard": "^1.0", "nextcloud/ocp": "dev-master", "phpunit/phpunit": "^9.5", - "bamarni/composer-bin-plugin": "^1.8" + "bamarni/composer-bin-plugin": "^1.8", + "guzzlehttp/guzzle": "^7.9" }, "license": "AGPLv3", "authors": [ diff --git a/composer.lock b/composer.lock index 188820ce45..cffe4e1a0f 100644 --- a/composer.lock +++ b/composer.lock @@ -4,8 +4,1008 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "dfca2194cfc2b7e48d963f85fa6d2b26", - "packages": [], + "content-hash": "0e5129d8227189f94380aa1b094bda83", + "packages": [ + { + "name": "champs-libres/wopi-lib", + "version": "dev-master", + "source": { + "type": "git", + "url": "https://github.com/Champs-Libres/wopi-lib.git", + "reference": "922e68dd1b1f769c7c0386e3b5f93fc64258cc0f" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/Champs-Libres/wopi-lib/zipball/922e68dd1b1f769c7c0386e3b5f93fc64258cc0f", + "reference": "922e68dd1b1f769c7c0386e3b5f93fc64258cc0f", + "shasum": "" + }, + "require": { + "ext-json": "*", + "ext-openssl": "*", + "ext-simplexml": "*", + "loophp/psr17": "^1.0", + "php": ">= 7.4", + "phpseclib/phpseclib": "^3.0", + "psr/cache": "^1.0 || ^2.0 || ^3.0", + "psr/http-client": "^1.0", + "psr/http-client-implementation": "^1", + "psr/http-factory": "^1.0.1", + "psr/http-factory-implementation": "^1", + "psr/http-message": "^1.0", + "psr/http-message-implementation": "^1" + }, + "require-dev": { + "drupol/php-conventions": "^5.0", + "friends-of-phpspec/phpspec-code-coverage": "^6.1", + "nyholm/psr7": "^1.4", + "phpspec/phpspec": "^7.1", + "symfony/http-client": "^5.3" + }, + "default-branch": true, + "type": "library", + "autoload": { + "psr-4": { + "ChampsLibres\\WopiLib\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "A standard and framework agnostic PHP library to facilitate the implementation of the WOPI protocol.", + "homepage": "http://github.com/champs-libres/wopi-lib", + "keywords": [ + "wopi" + ], + "support": { + "docs": "https://github.com/champs-libres/wopi-lib", + "issues": "https://github.com/champs-libres/wopi-lib/issues", + "source": "https://github.com/champs-libres/wopi-lib" + }, + "time": "2023-01-10T20:26:46+00:00" + }, + { + "name": "guzzlehttp/guzzle", + "version": "7.9.2", + "source": { + "type": "git", + "url": "https://github.com/guzzle/guzzle.git", + "reference": "d281ed313b989f213357e3be1a179f02196ac99b" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/guzzle/guzzle/zipball/d281ed313b989f213357e3be1a179f02196ac99b", + "reference": "d281ed313b989f213357e3be1a179f02196ac99b", + "shasum": "" + }, + "require": { + "ext-json": "*", + "guzzlehttp/promises": "^1.5.3 || ^2.0.3", + "guzzlehttp/psr7": "^2.7.0", + "php": "^7.2.5 || ^8.0", + "psr/http-client": "^1.0", + "symfony/deprecation-contracts": "^2.2 || ^3.0" + }, + "provide": { + "psr/http-client-implementation": "1.0" + }, + "require-dev": { + "bamarni/composer-bin-plugin": "^1.8.2", + "ext-curl": "*", + "guzzle/client-integration-tests": "3.0.2", + "php-http/message-factory": "^1.1", + "phpunit/phpunit": "^8.5.39 || ^9.6.20", + "psr/log": "^1.1 || ^2.0 || ^3.0" + }, + "suggest": { + "ext-curl": "Required for CURL handler support", + "ext-intl": "Required for Internationalized Domain Name (IDN) support", + "psr/log": "Required for using the Log middleware" + }, + "type": "library", + "extra": { + "bamarni-bin": { + "bin-links": true, + "forward-command": false + } + }, + "autoload": { + "files": [ + "src/functions_include.php" + ], + "psr-4": { + "GuzzleHttp\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Graham Campbell", + "email": "hello@gjcampbell.co.uk", + "homepage": "https://github.com/GrahamCampbell" + }, + { + "name": "Michael Dowling", + "email": "mtdowling@gmail.com", + "homepage": "https://github.com/mtdowling" + }, + { + "name": "Jeremy Lindblom", + "email": "jeremeamia@gmail.com", + "homepage": "https://github.com/jeremeamia" + }, + { + "name": "George Mponos", + "email": "gmponos@gmail.com", + "homepage": "https://github.com/gmponos" + }, + { + "name": "Tobias Nyholm", + "email": "tobias.nyholm@gmail.com", + "homepage": "https://github.com/Nyholm" + }, + { + "name": "Márk Sági-Kazár", + "email": "mark.sagikazar@gmail.com", + "homepage": "https://github.com/sagikazarmark" + }, + { + "name": "Tobias Schultze", + "email": "webmaster@tubo-world.de", + "homepage": "https://github.com/Tobion" + } + ], + "description": "Guzzle is a PHP HTTP client library", + "keywords": [ + "client", + "curl", + "framework", + "http", + "http client", + "psr-18", + "psr-7", + "rest", + "web service" + ], + "support": { + "issues": "https://github.com/guzzle/guzzle/issues", + "source": "https://github.com/guzzle/guzzle/tree/7.9.2" + }, + "funding": [ + { + "url": "https://github.com/GrahamCampbell", + "type": "github" + }, + { + "url": "https://github.com/Nyholm", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/guzzlehttp/guzzle", + "type": "tidelift" + } + ], + "time": "2024-07-24T11:22:20+00:00" + }, + { + "name": "guzzlehttp/promises", + "version": "2.0.4", + "source": { + "type": "git", + "url": "https://github.com/guzzle/promises.git", + "reference": "f9c436286ab2892c7db7be8c8da4ef61ccf7b455" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/guzzle/promises/zipball/f9c436286ab2892c7db7be8c8da4ef61ccf7b455", + "reference": "f9c436286ab2892c7db7be8c8da4ef61ccf7b455", + "shasum": "" + }, + "require": { + "php": "^7.2.5 || ^8.0" + }, + "require-dev": { + "bamarni/composer-bin-plugin": "^1.8.2", + "phpunit/phpunit": "^8.5.39 || ^9.6.20" + }, + "type": "library", + "extra": { + "bamarni-bin": { + "bin-links": true, + "forward-command": false + } + }, + "autoload": { + "psr-4": { + "GuzzleHttp\\Promise\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Graham Campbell", + "email": "hello@gjcampbell.co.uk", + "homepage": "https://github.com/GrahamCampbell" + }, + { + "name": "Michael Dowling", + "email": "mtdowling@gmail.com", + "homepage": "https://github.com/mtdowling" + }, + { + "name": "Tobias Nyholm", + "email": "tobias.nyholm@gmail.com", + "homepage": "https://github.com/Nyholm" + }, + { + "name": "Tobias Schultze", + "email": "webmaster@tubo-world.de", + "homepage": "https://github.com/Tobion" + } + ], + "description": "Guzzle promises library", + "keywords": [ + "promise" + ], + "support": { + "issues": "https://github.com/guzzle/promises/issues", + "source": "https://github.com/guzzle/promises/tree/2.0.4" + }, + "funding": [ + { + "url": "https://github.com/GrahamCampbell", + "type": "github" + }, + { + "url": "https://github.com/Nyholm", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/guzzlehttp/promises", + "type": "tidelift" + } + ], + "time": "2024-10-17T10:06:22+00:00" + }, + { + "name": "guzzlehttp/psr7", + "version": "2.7.0", + "source": { + "type": "git", + "url": "https://github.com/guzzle/psr7.git", + "reference": "a70f5c95fb43bc83f07c9c948baa0dc1829bf201" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/guzzle/psr7/zipball/a70f5c95fb43bc83f07c9c948baa0dc1829bf201", + "reference": "a70f5c95fb43bc83f07c9c948baa0dc1829bf201", + "shasum": "" + }, + "require": { + "php": "^7.2.5 || ^8.0", + "psr/http-factory": "^1.0", + "psr/http-message": "^1.1 || ^2.0", + "ralouphie/getallheaders": "^3.0" + }, + "provide": { + "psr/http-factory-implementation": "1.0", + "psr/http-message-implementation": "1.0" + }, + "require-dev": { + "bamarni/composer-bin-plugin": "^1.8.2", + "http-interop/http-factory-tests": "0.9.0", + "phpunit/phpunit": "^8.5.39 || ^9.6.20" + }, + "suggest": { + "laminas/laminas-httphandlerrunner": "Emit PSR-7 responses" + }, + "type": "library", + "extra": { + "bamarni-bin": { + "bin-links": true, + "forward-command": false + } + }, + "autoload": { + "psr-4": { + "GuzzleHttp\\Psr7\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Graham Campbell", + "email": "hello@gjcampbell.co.uk", + "homepage": "https://github.com/GrahamCampbell" + }, + { + "name": "Michael Dowling", + "email": "mtdowling@gmail.com", + "homepage": "https://github.com/mtdowling" + }, + { + "name": "George Mponos", + "email": "gmponos@gmail.com", + "homepage": "https://github.com/gmponos" + }, + { + "name": "Tobias Nyholm", + "email": "tobias.nyholm@gmail.com", + "homepage": "https://github.com/Nyholm" + }, + { + "name": "Márk Sági-Kazár", + "email": "mark.sagikazar@gmail.com", + "homepage": "https://github.com/sagikazarmark" + }, + { + "name": "Tobias Schultze", + "email": "webmaster@tubo-world.de", + "homepage": "https://github.com/Tobion" + }, + { + "name": "Márk Sági-Kazár", + "email": "mark.sagikazar@gmail.com", + "homepage": "https://sagikazarmark.hu" + } + ], + "description": "PSR-7 message implementation that also provides common utility methods", + "keywords": [ + "http", + "message", + "psr-7", + "request", + "response", + "stream", + "uri", + "url" + ], + "support": { + "issues": "https://github.com/guzzle/psr7/issues", + "source": "https://github.com/guzzle/psr7/tree/2.7.0" + }, + "funding": [ + { + "url": "https://github.com/GrahamCampbell", + "type": "github" + }, + { + "url": "https://github.com/Nyholm", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/guzzlehttp/psr7", + "type": "tidelift" + } + ], + "time": "2024-07-18T11:15:46+00:00" + }, + { + "name": "loophp/psr17", + "version": "1.0.5", + "source": { + "type": "git", + "url": "https://github.com/loophp/psr17.git", + "reference": "641f2464af9d581b1cdbe1a5a1f5e98db98d3a6e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/loophp/psr17/zipball/641f2464af9d581b1cdbe1a5a1f5e98db98d3a6e", + "reference": "641f2464af9d581b1cdbe1a5a1f5e98db98d3a6e", + "shasum": "" + }, + "require": { + "php": ">= 7.4", + "psr/http-factory": "^1.0", + "psr/http-factory-implementation": "^1.0", + "psr/http-message": "^1.0 || ^2.0", + "psr/http-message-implementation": "^1.0" + }, + "require-dev": { + "drupol/php-conventions": "^5", + "ext-pcov": "*", + "friends-of-phpspec/phpspec-code-coverage": "^6", + "infection/infection": "^0.23 || ^0.24 || ^0.26", + "infection/phpspec-adapter": "^0.1.1 || ^0.2.0", + "nyholm/psr7": "^1.8", + "phpspec/phpspec": "^7.1" + }, + "suggest": { + "nyholm/psr7": "A super lightweight PSR-7 implementation" + }, + "type": "library", + "autoload": { + "psr-4": { + "loophp\\psr17\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Pol Dellaiera", + "email": "pol.dellaiera@protonmail.com" + } + ], + "description": "Provides a PSR17 synthetic implementation.", + "homepage": "http://github.com/loophp/psr17", + "keywords": [ + "factory", + "psr-17" + ], + "support": { + "docs": "https://github.com/loophp/psr17", + "issues": "https://github.com/loophp/psr17/issues", + "source": "https://github.com/loophp/psr17" + }, + "funding": [ + { + "url": "https://github.com/drupol", + "type": "github" + } + ], + "time": "2023-05-04T18:41:45+00:00" + }, + { + "name": "paragonie/constant_time_encoding", + "version": "v3.0.0", + "source": { + "type": "git", + "url": "https://github.com/paragonie/constant_time_encoding.git", + "reference": "df1e7fde177501eee2037dd159cf04f5f301a512" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/paragonie/constant_time_encoding/zipball/df1e7fde177501eee2037dd159cf04f5f301a512", + "reference": "df1e7fde177501eee2037dd159cf04f5f301a512", + "shasum": "" + }, + "require": { + "php": "^8" + }, + "require-dev": { + "phpunit/phpunit": "^9", + "vimeo/psalm": "^4|^5" + }, + "type": "library", + "autoload": { + "psr-4": { + "ParagonIE\\ConstantTime\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Paragon Initiative Enterprises", + "email": "security@paragonie.com", + "homepage": "https://paragonie.com", + "role": "Maintainer" + }, + { + "name": "Steve 'Sc00bz' Thomas", + "email": "steve@tobtu.com", + "homepage": "https://www.tobtu.com", + "role": "Original Developer" + } + ], + "description": "Constant-time Implementations of RFC 4648 Encoding (Base-64, Base-32, Base-16)", + "keywords": [ + "base16", + "base32", + "base32_decode", + "base32_encode", + "base64", + "base64_decode", + "base64_encode", + "bin2hex", + "encoding", + "hex", + "hex2bin", + "rfc4648" + ], + "support": { + "email": "info@paragonie.com", + "issues": "https://github.com/paragonie/constant_time_encoding/issues", + "source": "https://github.com/paragonie/constant_time_encoding" + }, + "time": "2024-05-08T12:36:18+00:00" + }, + { + "name": "paragonie/random_compat", + "version": "v9.99.100", + "source": { + "type": "git", + "url": "https://github.com/paragonie/random_compat.git", + "reference": "996434e5492cb4c3edcb9168db6fbb1359ef965a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/paragonie/random_compat/zipball/996434e5492cb4c3edcb9168db6fbb1359ef965a", + "reference": "996434e5492cb4c3edcb9168db6fbb1359ef965a", + "shasum": "" + }, + "require": { + "php": ">= 7" + }, + "require-dev": { + "phpunit/phpunit": "4.*|5.*", + "vimeo/psalm": "^1" + }, + "suggest": { + "ext-libsodium": "Provides a modern crypto API that can be used to generate random bytes." + }, + "type": "library", + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Paragon Initiative Enterprises", + "email": "security@paragonie.com", + "homepage": "https://paragonie.com" + } + ], + "description": "PHP 5.x polyfill for random_bytes() and random_int() from PHP 7", + "keywords": [ + "csprng", + "polyfill", + "pseudorandom", + "random" + ], + "support": { + "email": "info@paragonie.com", + "issues": "https://github.com/paragonie/random_compat/issues", + "source": "https://github.com/paragonie/random_compat" + }, + "time": "2020-10-15T08:29:30+00:00" + }, + { + "name": "phpseclib/phpseclib", + "version": "3.0.42", + "source": { + "type": "git", + "url": "https://github.com/phpseclib/phpseclib.git", + "reference": "db92f1b1987b12b13f248fe76c3a52cadb67bb98" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phpseclib/phpseclib/zipball/db92f1b1987b12b13f248fe76c3a52cadb67bb98", + "reference": "db92f1b1987b12b13f248fe76c3a52cadb67bb98", + "shasum": "" + }, + "require": { + "paragonie/constant_time_encoding": "^1|^2|^3", + "paragonie/random_compat": "^1.4|^2.0|^9.99.99", + "php": ">=5.6.1" + }, + "require-dev": { + "phpunit/phpunit": "*" + }, + "suggest": { + "ext-dom": "Install the DOM extension to load XML formatted public keys.", + "ext-gmp": "Install the GMP (GNU Multiple Precision) extension in order to speed up arbitrary precision integer arithmetic operations.", + "ext-libsodium": "SSH2/SFTP can make use of some algorithms provided by the libsodium-php extension.", + "ext-mcrypt": "Install the Mcrypt extension in order to speed up a few other cryptographic operations.", + "ext-openssl": "Install the OpenSSL extension in order to speed up a wide variety of cryptographic operations." + }, + "type": "library", + "autoload": { + "files": [ + "phpseclib/bootstrap.php" + ], + "psr-4": { + "phpseclib3\\": "phpseclib/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Jim Wigginton", + "email": "terrafrost@php.net", + "role": "Lead Developer" + }, + { + "name": "Patrick Monnerat", + "email": "pm@datasphere.ch", + "role": "Developer" + }, + { + "name": "Andreas Fischer", + "email": "bantu@phpbb.com", + "role": "Developer" + }, + { + "name": "Hans-Jürgen Petrich", + "email": "petrich@tronic-media.com", + "role": "Developer" + }, + { + "name": "Graham Campbell", + "email": "graham@alt-three.com", + "role": "Developer" + } + ], + "description": "PHP Secure Communications Library - Pure-PHP implementations of RSA, AES, SSH2, SFTP, X.509 etc.", + "homepage": "http://phpseclib.sourceforge.net", + "keywords": [ + "BigInteger", + "aes", + "asn.1", + "asn1", + "blowfish", + "crypto", + "cryptography", + "encryption", + "rsa", + "security", + "sftp", + "signature", + "signing", + "ssh", + "twofish", + "x.509", + "x509" + ], + "support": { + "issues": "https://github.com/phpseclib/phpseclib/issues", + "source": "https://github.com/phpseclib/phpseclib/tree/3.0.42" + }, + "funding": [ + { + "url": "https://github.com/terrafrost", + "type": "github" + }, + { + "url": "https://www.patreon.com/phpseclib", + "type": "patreon" + }, + { + "url": "https://tidelift.com/funding/github/packagist/phpseclib/phpseclib", + "type": "tidelift" + } + ], + "time": "2024-09-16T03:06:04+00:00" + }, + { + "name": "psr/cache", + "version": "3.0.0", + "source": { + "type": "git", + "url": "https://github.com/php-fig/cache.git", + "reference": "aa5030cfa5405eccfdcb1083ce040c2cb8d253bf" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/cache/zipball/aa5030cfa5405eccfdcb1083ce040c2cb8d253bf", + "reference": "aa5030cfa5405eccfdcb1083ce040c2cb8d253bf", + "shasum": "" + }, + "require": { + "php": ">=8.0.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Cache\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "Common interface for caching libraries", + "keywords": [ + "cache", + "psr", + "psr-6" + ], + "support": { + "source": "https://github.com/php-fig/cache/tree/3.0.0" + }, + "time": "2021-02-03T23:26:27+00:00" + }, + { + "name": "psr/http-client", + "version": "1.0.3", + "source": { + "type": "git", + "url": "https://github.com/php-fig/http-client.git", + "reference": "bb5906edc1c324c9a05aa0873d40117941e5fa90" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/http-client/zipball/bb5906edc1c324c9a05aa0873d40117941e5fa90", + "reference": "bb5906edc1c324c9a05aa0873d40117941e5fa90", + "shasum": "" + }, + "require": { + "php": "^7.0 || ^8.0", + "psr/http-message": "^1.0 || ^2.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Http\\Client\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "Common interface for HTTP clients", + "homepage": "https://github.com/php-fig/http-client", + "keywords": [ + "http", + "http-client", + "psr", + "psr-18" + ], + "support": { + "source": "https://github.com/php-fig/http-client" + }, + "time": "2023-09-23T14:17:50+00:00" + }, + { + "name": "psr/http-factory", + "version": "1.1.0", + "source": { + "type": "git", + "url": "https://github.com/php-fig/http-factory.git", + "reference": "2b4765fddfe3b508ac62f829e852b1501d3f6e8a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/http-factory/zipball/2b4765fddfe3b508ac62f829e852b1501d3f6e8a", + "reference": "2b4765fddfe3b508ac62f829e852b1501d3f6e8a", + "shasum": "" + }, + "require": { + "php": ">=7.1", + "psr/http-message": "^1.0 || ^2.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Http\\Message\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "PSR-17: Common interfaces for PSR-7 HTTP message factories", + "keywords": [ + "factory", + "http", + "message", + "psr", + "psr-17", + "psr-7", + "request", + "response" + ], + "support": { + "source": "https://github.com/php-fig/http-factory" + }, + "time": "2024-04-15T12:06:14+00:00" + }, + { + "name": "psr/http-message", + "version": "1.1", + "source": { + "type": "git", + "url": "https://github.com/php-fig/http-message.git", + "reference": "cb6ce4845ce34a8ad9e68117c10ee90a29919eba" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/http-message/zipball/cb6ce4845ce34a8ad9e68117c10ee90a29919eba", + "reference": "cb6ce4845ce34a8ad9e68117c10ee90a29919eba", + "shasum": "" + }, + "require": { + "php": "^7.2 || ^8.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.1.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Http\\Message\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "http://www.php-fig.org/" + } + ], + "description": "Common interface for HTTP messages", + "homepage": "https://github.com/php-fig/http-message", + "keywords": [ + "http", + "http-message", + "psr", + "psr-7", + "request", + "response" + ], + "support": { + "source": "https://github.com/php-fig/http-message/tree/1.1" + }, + "time": "2023-04-04T09:50:52+00:00" + }, + { + "name": "ralouphie/getallheaders", + "version": "3.0.3", + "source": { + "type": "git", + "url": "https://github.com/ralouphie/getallheaders.git", + "reference": "120b605dfeb996808c31b6477290a714d356e822" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/ralouphie/getallheaders/zipball/120b605dfeb996808c31b6477290a714d356e822", + "reference": "120b605dfeb996808c31b6477290a714d356e822", + "shasum": "" + }, + "require": { + "php": ">=5.6" + }, + "require-dev": { + "php-coveralls/php-coveralls": "^2.1", + "phpunit/phpunit": "^5 || ^6.5" + }, + "type": "library", + "autoload": { + "files": [ + "src/getallheaders.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Ralph Khattar", + "email": "ralph.khattar@gmail.com" + } + ], + "description": "A polyfill for getallheaders.", + "support": { + "issues": "https://github.com/ralouphie/getallheaders/issues", + "source": "https://github.com/ralouphie/getallheaders/tree/develop" + }, + "time": "2019-03-08T08:55:37+00:00" + }, + { + "name": "symfony/deprecation-contracts", + "version": "v2.5.4", + "source": { + "type": "git", + "url": "https://github.com/symfony/deprecation-contracts.git", + "reference": "605389f2a7e5625f273b53960dc46aeaf9c62918" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/deprecation-contracts/zipball/605389f2a7e5625f273b53960dc46aeaf9c62918", + "reference": "605389f2a7e5625f273b53960dc46aeaf9c62918", + "shasum": "" + }, + "require": { + "php": ">=7.1" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "2.5-dev" + }, + "thanks": { + "name": "symfony/contracts", + "url": "https://github.com/symfony/contracts" + } + }, + "autoload": { + "files": [ + "function.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "A generic function and convention to trigger deprecation notices", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/deprecation-contracts/tree/v2.5.4" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-09-25T14:11:13+00:00" + } + ], "packages-dev": [ { "name": "bamarni/composer-bin-plugin", @@ -3107,8 +4107,8 @@ "aliases": [], "minimum-stability": "dev", "stability-flags": { - "nextcloud/ocp": 20, - "roave/security-advisories": 20 + "roave/security-advisories": 20, + "nextcloud/ocp": 20 }, "prefer-stable": true, "prefer-lowest": false, @@ -3116,7 +4116,7 @@ "ext-json": "*", "ext-simplexml": "*" }, - "platform-dev": {}, + "platform-dev": [], "platform-overrides": { "php": "8.0" }, diff --git a/lib/Command/ActivateConfig.php b/lib/Command/ActivateConfig.php index de25ecd19a..6ab9c7ca35 100644 --- a/lib/Command/ActivateConfig.php +++ b/lib/Command/ActivateConfig.php @@ -64,13 +64,16 @@ protected function execute(InputInterface $input, OutputInterface $output): int return 1; } - try { - $this->connectivityService->testCapabilities($output); - } catch (\Throwable $e) { - // FIXME: Optional when allowing generic WOPI servers - $output->writeln('Failed to fetch capabilities endpoint from ' . $this->capabilitiesService->getCapabilitiesEndpoint()); - $output->writeln($e->getMessage()); - return 1; + if ($this->connectivityService->hasCapabilities()) { + try { + $this->connectivityService->testCapabilities($output); + } catch (\Throwable $e) { + // FIXME: Optional when allowing generic WOPI servers + // We need this now + $output->writeln('Failed to fetch capabilities endpoint from ' . $this->capabilitiesService->getCapabilitiesEndpoint()); + $output->writeln($e->getMessage()); + return 1; + } } try { diff --git a/lib/Controller/DocumentController.php b/lib/Controller/DocumentController.php index ddc79a47b6..dd4977c424 100644 --- a/lib/Controller/DocumentController.php +++ b/lib/Controller/DocumentController.php @@ -382,6 +382,7 @@ public function editOnlineTarget(int $fileId, ?string $target = null): RedirectR } #[PublicPage] + #[NoCSRFRequired] public function token(int $fileId, ?string $shareToken = null, ?string $path = null, ?string $guestName = null): DataResponse { try { $share = $shareToken ? $this->shareManager->getShareByToken($shareToken) : null; diff --git a/lib/Controller/WopiController.php b/lib/Controller/WopiController.php index de42fbc648..91bc93f89d 100644 --- a/lib/Controller/WopiController.php +++ b/lib/Controller/WopiController.php @@ -132,7 +132,7 @@ public function checkFileInfo(string $fileId, string $access_token): JSONRespons $response = [ 'BaseFileName' => $file->getName(), 'Size' => $file->getSize(), - 'Version' => $version, + 'Version' => $file->getEtag(), 'UserId' => !$isPublic ? $wopi->getEditorUid() : $guestUserId, 'OwnerId' => $wopi->getOwnerUid(), 'UserFriendlyName' => $userDisplayName, @@ -227,6 +227,20 @@ public function checkFileInfo(string $fileId, string $access_token): JSONRespons $file )); + // make checkfileinfo pass plain wopi + $customProperties = ["UserExtraInfo","UserPrivateInfo","EnableInsertRemoteImage","EnableInsertRemoteFile","EnableShare","HideUserList","EnableOwnerTermination","DisableExport","DisableCopy","HideExportOption","HidePrintOption","DownloadAsPostMessage","IsUserLocked","EnableRemoteLinkPicker","HasContentRange","IsAdminUser"]; + foreach ($customProperties as $property) { + unset($response[$property]); + } + + $response['SupportsUpdate'] = true; + $response['SupportsGetLock'] = $response['SupportsLocks']; + $response['SupportsContainers'] = false; + $response['SupportsDeleteFile'] = false; + $response['UserCanNotWriteRelative'] = false; + // $response['SupportedShareUrlTypes'] = ''; + // $response['FileUrl'] = ''; + return new JSONResponse($response); } @@ -278,7 +292,7 @@ private function setFederationFileInfo(Wopi $wopi, $response) { #[NoCSRFRequired] #[PublicPage] #[FrontpageRoute(verb: 'GET', url: 'wopi/files/{fileId}/contents')] - public function getFile(string $fileId, string $access_token): JSONResponse|StreamResponse { + public function getFile(string $fileId, string $access_token): JSONResponse|StreamResponse|Http\Response { [$fileId, , $version] = Helper::parseFileId($fileId); try { @@ -342,6 +356,7 @@ public function getFile(string $fileId, string $access_token): JSONResponse|Stre } } } + $response->addHeader('X-WOPI-ItemVersion', $file->getEtag()); $response->addHeader('Content-Disposition', 'attachment'); $response->addHeader('Content-Type', 'application/octet-stream'); return $response; @@ -466,7 +481,9 @@ public function putFile(string $fileId, string $access_token): JSONResponse { $wopi->setTemplateId(null); $this->wopiMapper->update($wopi); } - return new JSONResponse(['LastModifiedTime' => Helper::toISO8601($file->getMTime())]); + $response = new JSONResponse(['LastModifiedTime' => Helper::toISO8601($file->getMTime())]); + $response->addHeader('X-WOPI-ItemVersion', $file->getEtag()); + return $response; } catch (NotFoundException $e) { $this->logger->warning($e->getMessage(), ['exception' => $e]); return new JSONResponse([], Http::STATUS_NOT_FOUND); @@ -508,20 +525,25 @@ public function postFile(string $fileId, string $access_token): JSONResponse { return new JSONResponse([], Http::STATUS_FORBIDDEN); } + $response = null; switch ($wopiOverride) { case 'LOCK': - return $this->lock($wopi, $wopiLock); + $response = $this->lock($wopi, $wopiLock); case 'UNLOCK': - return $this->unlock($wopi, $wopiLock); + $response = $this->unlock($wopi, $wopiLock); case 'REFRESH_LOCK': - return $this->refreshLock($wopi, $wopiLock); + $response = $this->refreshLock($wopi, $wopiLock); case 'GET_LOCK': - return $this->getLock($wopi, $wopiLock); + $response = $this->getLock($wopi, $wopiLock); case 'RENAME_FILE': break; //FIXME: Move to function default: break; //FIXME: Move to function and add error for unsupported method } + if ($response !== null) { + $response->addHeader('X-WOPI-ItemVersion', $this->getFileForWopiToken($wopi)->getEtag()); + return $response; + } $isRenameFile = ($this->request->getHeader('X-WOPI-Override') === 'RENAME_FILE'); @@ -699,7 +721,9 @@ private function refreshLock(Wopi $wopi, string $lock): JSONResponse { private function getLock(Wopi $wopi, string $lock): JSONResponse { $locks = $this->lockManager->getLocks($wopi->getFileid()); - return new JSONResponse(); + $response = new JSONResponse(); + $response->addHeader('X-WOPI-Lock', $lock); + return $response; } /** diff --git a/lib/Middleware/WOPIMiddleware.php b/lib/Middleware/WOPIMiddleware.php index de5aa2322f..48e13955af 100644 --- a/lib/Middleware/WOPIMiddleware.php +++ b/lib/Middleware/WOPIMiddleware.php @@ -19,6 +19,7 @@ use OCP\AppFramework\Http\JSONResponse; use OCP\AppFramework\Http\Response; use OCP\AppFramework\Middleware; +use OCP\Files\NotFoundException; use OCP\Files\NotPermittedException; use OCP\IConfig; use OCP\IRequest; @@ -78,7 +79,11 @@ public function beforeController($controller, $methodName) { public function afterException($controller, $methodName, \Exception $exception): Response { if ($exception instanceof NotPermittedException && $controller instanceof WopiController) { - return new JSONResponse([], Http::STATUS_FORBIDDEN); + return new JSONResponse([], Http::STATUS_UNAUTHORIZED); + } + + if ($exception instanceof NotFoundException && $controller instanceof WopiController) { + return new JSONResponse([], Http::STATUS_NOT_FOUND); } if ($controller instanceof WopiController) { diff --git a/lib/Service/ConnectivityService.php b/lib/Service/ConnectivityService.php index fd19fb16f1..72c451f8d7 100644 --- a/lib/Service/ConnectivityService.php +++ b/lib/Service/ConnectivityService.php @@ -32,11 +32,23 @@ public function testDiscovery(OutputInterface $output): void { $output->writeln('✓ Valid mimetype response'); // FIXME: Optional when allowing generic WOPI servers - $this->parser->getUrlSrcValue('Capabilities'); - $output->writeln('✓ Valid capabilities entry'); + if ($this->hasCapabilities()) { + $output->writeln('✓ Valid capabilities entry'); + } + } + + public function hasCapabilities() : bool { + try { + return $this->parser->getUrlSrcValue('Capabilities') !== ''; + } catch (\Throwable) { + return false; + } } public function testCapabilities(OutputInterface $output): void { + if (!$this->hasCapabilities()) { + return; + } $this->capabilitiesService->resetCache(); $this->capabilitiesService->fetch(); $output->writeln('✓ Fetched /hosting/capabilities endpoint'); @@ -57,6 +69,10 @@ public function testCapabilities(OutputInterface $output): void { public function autoConfigurePublicUrl(): void { $determinedUrl = $this->parser->getUrlSrcValue('application/vnd.openxmlformats-officedocument.wordprocessingml.document'); $detectedUrl = $this->appConfig->domainOnly($determinedUrl); + if ($detectedUrl === '') { + $determinedUrl = $this->parser->getUrlSrcByExtension('internal-http', 'docx', 'edit'); + $detectedUrl = $this->appConfig->domainOnly($determinedUrl); + } $this->appConfig->setAppValue('public_wopi_url', $detectedUrl); } } diff --git a/lib/TokenManager.php b/lib/TokenManager.php index 674fd563e7..aeeeb29047 100644 --- a/lib/TokenManager.php +++ b/lib/TokenManager.php @@ -269,6 +269,6 @@ public function setGuestName(Wopi $wopi, ?string $guestName = null): Wopi { } public function getUrlSrc(File $file): string { - return $this->wopiParser->getUrlSrcValue($file->getMimeType()); + return $this->wopiParser->getUrlSrcForFile($file); } } diff --git a/lib/WOPI/Parser.php b/lib/WOPI/Parser.php index f34edf9dee..f01904e0fa 100644 --- a/lib/WOPI/Parser.php +++ b/lib/WOPI/Parser.php @@ -8,12 +8,126 @@ use Exception; use OCA\Richdocuments\Service\DiscoveryService; +use OCP\Files\File; +use OCP\IL10N; +use OCP\IRequest; use Psr\Log\LoggerInterface; +use SimpleXMLElement; class Parser { + + public const ACTION_EDIT = 'edit'; + public const ACTION_VIEW = 'view'; + public const ACTION_EDITNEW = 'editnew'; + + // https://wopi.readthedocs.io/en/latest/faq/languages.html + public const SUPPORTED_LANGUAGES = [ + 'af-ZA', + 'am-ET', + 'ar-SA', + 'as-IN', + 'az-Latn-AZ', + 'be-BY', + 'bg-BG', + 'bn-BD', + 'bn-IN', + 'bs-Latn-BA', + 'ca-ES', + 'ca-ES-valencia', + 'chr-Cher-US', + 'cs-CZ', + 'cy-GB', + 'da-DK', + 'de-DE', + 'el-GR', + 'en-gb', + 'en-US', + 'es-ES', + 'es-mx', + 'et-EE', + 'eu-ES', + 'fa-IR', + 'fi-FI', + 'fil-PH', + 'fr-ca', + 'fr-FR', + 'ga-IE', + 'gd-GB', + 'gl-ES', + 'gu-IN', + 'ha-Latn-NG', + 'he-IL', + 'hi-IN', + 'hr-HR', + 'hu-HU', + 'hy-AM', + 'id-ID', + 'is-IS', + 'it-IT', + 'ja-JP', + 'ka-GE', + 'kk-KZ', + 'km-KH', + 'kn-IN', + 'kok-IN', + 'ko-KR', + 'ky-KG', + 'lb-LU', + 'lo-la', + 'lt-LT', + 'lv-LV', + 'mi-NZ', + 'mk-MK', + 'ml-IN', + 'mn-MN', + 'mr-IN', + 'ms-MY', + 'mt-MT', + 'nb-NO', + 'ne-NP', + 'nl-NL', + 'nn-NO', + 'or-IN', + 'pa-IN', + 'pl-PL', + 'prs-AF', + 'pt-BR', + 'pt-PT', + 'quz-PE', + 'ro-Ro', + 'ru-Ru', + 'sd-Arab-PK', + 'si-LK', + 'sk-SK', + 'sl-SI', + 'sq-AL', + 'sr-Cyrl-BA', + 'sr-Cyrl-RS', + 'sr-Latn-RS', + 'sv-SE', + 'sw-KE', + 'ta-IN', + 'te-IN', + 'th-TH', + 'tk-TM', + 'tr-TR', + 'tt-RU', + 'ug-CN', + 'uk-UA', + 'ur-PK', + 'uz-Latn-UZ', + 'vi-VN', + 'zh-CN', + 'zh-TW' + ]; + + private ?SimpleXMLElement $parsed = null; + public function __construct( private DiscoveryService $discoveryService, private LoggerInterface $logger, + private IL10N $l10n, + private IRequest $request, ) { } @@ -34,11 +148,7 @@ public function getUrlSrcValue(string $appName): string { * @throws Exception */ private function getUrlSrc(string $mimetype): array { - $discovery = $this->discoveryService->get(); - $this->logger->debug('WOPI::getUrlSrc discovery: {discovery}', ['discovery' => $discovery]); - $discoveryParsed = simplexml_load_string($discovery); - - $result = $discoveryParsed->xpath(sprintf('/wopi-discovery/net-zone/app[@name=\'%s\']/action', $mimetype)); + $result = $this->getParsed()->xpath(sprintf('/wopi-discovery/net-zone/app[@name=\'%s\']/action', $mimetype)); if ($result && count($result) > 0) { return [ 'urlsrc' => (string)$result[0]['urlsrc'], @@ -46,7 +156,136 @@ private function getUrlSrc(string $mimetype): array { ]; } - $this->logger->error('Didn\'t find urlsrc for mimetype {mimetype} in this WOPI discovery response: {discovery}', ['mimetype' => $mimetype, 'discovery' => $discovery]); + if ($this->getUrlSrcByExtension('internal-http', 'docx', 'edit')) { + return [ + 'urlsrc' => (string)$this->getUrlSrcByExtension('external-http', 'docx', 'edit'), + 'action' => 'edit', + ]; + } + + $this->logger->error('Didn\'t find urlsrc for mimetype {mimetype} in this WOPI discovery response', ['mimetype' => $mimetype]); throw new Exception('Could not find urlsrc for ' . $mimetype . ' in WOPI discovery response'); } + + /** + * @return SimpleXMLElement|bool + * @throws \Exception + */ + public function getParsed() { + if (!empty($this->parsed)) { + return $this->parsed; + } + $discovery = $this->discoveryService->get(); + $discoveryParsed = simplexml_load_string($discovery); + if ($discoveryParsed === false) { + throw new Exception('Discovery response is not valid XML'); + } + $this->parsed = $discoveryParsed; + return $discoveryParsed; + } + + public function getUrlSrcForFile(File $file, bool $edit = true): string { + $protocol = $this->request->getServerProtocol(); + $fallbackProtocol = $protocol === 'https' ? 'http' : 'https'; + + $netZones = [ + 'external-' . $protocol, + 'internal-' . $protocol, + 'external-' . $fallbackProtocol, + 'internal-' . $fallbackProtocol, + ]; + + $actions = [ + $edit && $file->getSize() === 0 ? self::ACTION_EDITNEW : null, + $edit ? self::ACTION_EDIT : null, + self::ACTION_VIEW, + ]; + $actions = array_filter($actions); + + foreach ($netZones as $netZone) { + foreach ($actions as $action) { + $result = $this->getUrlSrcByExtension($netZone, $file->getExtension(), $action); + if ($result) { + return $this->replaceUrlSrcParams($result); + } + } + } + + foreach ($netZones as $netZone) { + $result = $this->getUrlSrcByMimetype($netZone, $file->getMimeType()); + if ($result) { + return $this->replaceUrlSrcParams($result); + } + } + + throw new \Exception('Could not find urlsrc in WOPI'); + } + + public function getUrlSrcByExtension(string $netZoneName, string $actionExt, $actionName): ?string { + $result = $this->getParsed()->xpath(sprintf( + '/wopi-discovery/net-zone[@name=\'%s\']/app/action[@ext=\'%s\' and @name=\'%s\']', + $netZoneName, $actionExt, $actionName + )); + + if (!$result) { + return null; + } + + return (string)current($result)->attributes()['urlsrc']; + } + + private function getUrlSrcByMimetype(string $netZoneName, string $mimetype): ?string { + $result = $this->getParsed()->xpath(sprintf( + '/wopi-discovery/net-zone[@name=\'%s\']/app[@name=\'%s\']/action', + $netZoneName, $mimetype + )); + + if (!$result) { + return null; + } + + return (string)current($result)->attributes()['urlsrc']; + } + + private function replaceUrlSrcParams(string $urlSrc): string { + if (strpos($urlSrc, 'UI_LLCC') === false) { + return $urlSrc; + } + + $urlSrc = preg_replace('//', 'ui=' . $this->getLanguageCode() . '&', $urlSrc); + return preg_replace('/<.+>/', '', $urlSrc); + } + + private function getLanguageCode(): string { + $languageCode = $this->l10n->getLanguageCode(); + $localeCode = $this->l10n->getLocaleCode(); + $splitLocale = explode('_', $localeCode); + if (count($splitLocale) > 1) { + $localeCode = $splitLocale[1]; + } + + $languageMatches = array_filter(self::SUPPORTED_LANGUAGES, function ($language) use ($languageCode, $localeCode) { + return stripos($language, $languageCode) === 0; + }); + + // Unique match on the language + if (count($languageMatches) === 1) { + return array_shift($languageMatches); + } + $localeMatches = array_filter($languageMatches, function ($language) use ($languageCode, $localeCode) { + return stripos($language, $languageCode . '-' . $localeCode) === 0; + }); + + // Matches with language and locale with region + if (count($localeMatches) >= 1) { + return array_shift($localeMatches); + } + + // Fallback to first language match if multiple found and no fitting region is available + if (count($languageMatches) > 1) { + return array_shift($languageMatches); + } + + return 'en-US'; + } } diff --git a/src/view/Office.vue b/src/view/Office.vue index 2b68ae8049..1c68da157a 100644 --- a/src/view/Office.vue +++ b/src/view/Office.vue @@ -202,13 +202,16 @@ export default { }, computed: { showIframe() { - return this.loading >= LOADING_STATE.FRAME_READY || this.debug + return this.loading >= LOADING_STATE.FRAME_READY || this.debug || this.isPlainWopi }, iframeTitle() { return loadState('richdocuments', 'productName', 'Nextcloud Office') }, + isPlainWopi() { + return getCapabilities().collabora.length === 0 + }, showLoadingIndicator() { - return this.loading < LOADING_STATE.FRAME_READY + return this.loading < LOADING_STATE.FRAME_READY && !this.isPlainWopi }, errorMessage() { switch (parseInt(this.error)) { diff --git a/tests/wopi-test.sh b/tests/wopi-test.sh new file mode 100755 index 0000000000..2386e90e71 --- /dev/null +++ b/tests/wopi-test.sh @@ -0,0 +1,42 @@ +#!/bin/bash +# This script allows to run wopi-validator-core in a docker container +# on a Nextcloud instance with the richdocuments app enabled +# It is required to have a working WOPI server configured to generate a token +# +# For running locally as the docker container needs to be able to reach the Nextcloud instance +# NEXTCLOUD_URL=http://nextcloud.local DOCKER_ARGS="--network nextcloud_default" HOST_IP=192.168.21.1 ./tests/wopi-test.sh + +# current timestamp as filename +FILENAME=$(date +%s) + +NEXTCLOUD_URL=${NEXTCLOUD_URL:-http://nextcloud.local} +USERNAME=${USERNAME:-admin} +PASSWORD=${PASSWORD:-admin} +HOST_IP=${HOST_IP:-172.17.0.1} +DOCKER_ARGS=${DOCKER_ARGS:-} + +curl $NEXTCLOUD_URL/status.php --max-time 5 --retry 5 --retry-delay 0 --retry-max-time 30 --retry-connrefused + +curl -X PUT -u $USERNAME:$PASSWORD $NEXTCLOUD_URL/remote.php/webdav/$FILENAME.odt -s + +PROPFIND_RESULT=$(curl -X PROPFIND -u $USERNAME:$PASSWORD $NEXTCLOUD_URL/remote.php/webdav/$FILENAME.odt --data '' -s) +FILE_ID=$(echo $PROPFIND_RESULT | grep -ohE '(.*)' | cut -d'>' -f 2 | cut -d'<' -f 1) + +COOKIEJAR=/tmp/cookie-jar +rm -f $COOKIEJAR +CSRF_TOKEN=$(curl $NEXTCLOUD_URL/index.php/login --cookie-jar $COOKIEJAR -s | grep requesttoken | grep -ohE 'data-requesttoken="(.*)"' | cut -d'"' -f 2) + +echo "File id: $FILE_ID" +echo "CSRF token: $CSRF_TOKEN" + +WOPI_TOKEN=$(curl -u $USERNAME:$PASSWORD --cookie-jar $COOKIEJAR $NEXTCLOUD_URL/index.php/apps/richdocuments/token?fileId=$FILE_ID -X POST -H "requesttoken: $CSRF_TOKEN" -s | jq -r .token) + +# Nextcloud needed a valid mimetype before ut the wopi test can only be ran with a .wopitest file +curl -X MOVE -u $USERNAME:$PASSWORD $NEXTCLOUD_URL/remote.php/webdav/$FILENAME.odt -H "Destination: $NEXTCLOUD_URL/remote.php/webdav/$FILENAME.wopitest" + +WOPI_URL="$NEXTCLOUD_URL/index.php/apps/richdocuments/wopi/files/$FILE_ID" + +echo "WOPI URL: $WOPI_URL" +echo "WOPI token generated: $WOPI_TOKEN" + +docker run $DOCKER_ARGS --add-host "nextcloud.local:${HOST_IP}" --rm tylerbutler/wopi-validator -- -w $WOPI_URL -t $WOPI_TOKEN -l 0 "$@"