generated from Nodarx/template
Add WOPI discovery service and connection diagnostics
This commit is contained in:
+16
@@ -0,0 +1,16 @@
|
||||
# Installed dependencies
|
||||
vendor/
|
||||
|
||||
# Local configuration and generated files
|
||||
.env
|
||||
.env.*
|
||||
*.local
|
||||
*.cache
|
||||
.phpunit.cache/
|
||||
coverage/
|
||||
*.log
|
||||
|
||||
# Editor settings
|
||||
.idea/
|
||||
.vscode/
|
||||
.DS_Store
|
||||
@@ -0,0 +1,13 @@
|
||||
#!/usr/bin/env php
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
// Usable before module installation/configuration; does not boot the application.
|
||||
$root = dirname(__DIR__, 3);
|
||||
$loader = require $root . '/vendor/autoload.php';
|
||||
$loader->addPsr4('KTXM\\ServiceWopi\\', dirname(__DIR__) . '/lib/');
|
||||
$application = new Symfony\Component\Console\Application('WOPI connection check');
|
||||
$application->addCommand(new KTXM\ServiceWopi\Console\CheckCommand(new KTXM\ServiceWopi\Discovery\DiscoveryClient()));
|
||||
$application->setDefaultCommand('wopi:check', true);
|
||||
exit($application->run());
|
||||
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"name": "ktrix/service-wopi",
|
||||
"description": "WOPI document server integration for Ktrix",
|
||||
"type": "library",
|
||||
"license": "AGPL-3.0-or-later",
|
||||
"require": {
|
||||
"php": ">=8.3",
|
||||
"ext-curl": "*",
|
||||
"ext-dom": "*",
|
||||
"ext-libxml": "*",
|
||||
"symfony/console": "^7.0"
|
||||
},
|
||||
"config": {
|
||||
"vendor-dir": "lib/vendor",
|
||||
"optimize-autoloader": true,
|
||||
"sort-packages": true
|
||||
}
|
||||
}
|
||||
Generated
+759
@@ -0,0 +1,759 @@
|
||||
{
|
||||
"_readme": [
|
||||
"This file locks the dependencies of your project to a known state",
|
||||
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
|
||||
"This file is @generated automatically"
|
||||
],
|
||||
"content-hash": "fc0fe7b30fb89947e64b3b462ddfcca8",
|
||||
"packages": [
|
||||
{
|
||||
"name": "psr/container",
|
||||
"version": "2.0.2",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/php-fig/container.git",
|
||||
"reference": "c71ecc56dfe541dbd90c5360474fbc405f8d5963"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/php-fig/container/zipball/c71ecc56dfe541dbd90c5360474fbc405f8d5963",
|
||||
"reference": "c71ecc56dfe541dbd90c5360474fbc405f8d5963",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"php": ">=7.4.0"
|
||||
},
|
||||
"type": "library",
|
||||
"extra": {
|
||||
"branch-alias": {
|
||||
"dev-master": "2.0.x-dev"
|
||||
}
|
||||
},
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"Psr\\Container\\": "src/"
|
||||
}
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"MIT"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "PHP-FIG",
|
||||
"homepage": "https://www.php-fig.org/"
|
||||
}
|
||||
],
|
||||
"description": "Common Container Interface (PHP FIG PSR-11)",
|
||||
"homepage": "https://github.com/php-fig/container",
|
||||
"keywords": [
|
||||
"PSR-11",
|
||||
"container",
|
||||
"container-interface",
|
||||
"container-interop",
|
||||
"psr"
|
||||
],
|
||||
"support": {
|
||||
"issues": "https://github.com/php-fig/container/issues",
|
||||
"source": "https://github.com/php-fig/container/tree/2.0.2"
|
||||
},
|
||||
"time": "2021-11-05T16:47:00+00:00"
|
||||
},
|
||||
{
|
||||
"name": "symfony/console",
|
||||
"version": "v7.4.18",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/symfony/console.git",
|
||||
"reference": "23d6f88a29f6d0eac45bd77d70307adf83ba7ab0"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/symfony/console/zipball/23d6f88a29f6d0eac45bd77d70307adf83ba7ab0",
|
||||
"reference": "23d6f88a29f6d0eac45bd77d70307adf83ba7ab0",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"php": ">=8.2",
|
||||
"symfony/deprecation-contracts": "^2.5|^3",
|
||||
"symfony/polyfill-mbstring": "~1.0",
|
||||
"symfony/service-contracts": "^2.5|^3",
|
||||
"symfony/string": "^7.2|^8.0"
|
||||
},
|
||||
"conflict": {
|
||||
"symfony/dependency-injection": "<6.4",
|
||||
"symfony/dotenv": "<6.4",
|
||||
"symfony/event-dispatcher": "<6.4",
|
||||
"symfony/lock": "<6.4",
|
||||
"symfony/process": "<6.4"
|
||||
},
|
||||
"provide": {
|
||||
"psr/log-implementation": "1.0|2.0|3.0"
|
||||
},
|
||||
"require-dev": {
|
||||
"psr/log": "^1|^2|^3",
|
||||
"symfony/config": "^6.4|^7.0|^8.0",
|
||||
"symfony/dependency-injection": "^6.4|^7.0|^8.0",
|
||||
"symfony/event-dispatcher": "^6.4|^7.0|^8.0",
|
||||
"symfony/http-foundation": "^6.4|^7.0|^8.0",
|
||||
"symfony/http-kernel": "^6.4|^7.0|^8.0",
|
||||
"symfony/lock": "^6.4|^7.0|^8.0",
|
||||
"symfony/messenger": "^6.4|^7.0|^8.0",
|
||||
"symfony/process": "^6.4|^7.0|^8.0",
|
||||
"symfony/stopwatch": "^6.4|^7.0|^8.0",
|
||||
"symfony/var-dumper": "^6.4|^7.0|^8.0"
|
||||
},
|
||||
"type": "library",
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"Symfony\\Component\\Console\\": ""
|
||||
},
|
||||
"exclude-from-classmap": [
|
||||
"/Tests/"
|
||||
]
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"MIT"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "Fabien Potencier",
|
||||
"email": "fabien@symfony.com"
|
||||
},
|
||||
{
|
||||
"name": "Symfony Community",
|
||||
"homepage": "https://symfony.com/contributors"
|
||||
}
|
||||
],
|
||||
"description": "Eases the creation of beautiful and testable command line interfaces",
|
||||
"homepage": "https://symfony.com",
|
||||
"keywords": [
|
||||
"cli",
|
||||
"command-line",
|
||||
"console",
|
||||
"terminal"
|
||||
],
|
||||
"support": {
|
||||
"source": "https://github.com/symfony/console/tree/v7.4.18"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
"url": "https://symfony.com/sponsor",
|
||||
"type": "custom"
|
||||
},
|
||||
{
|
||||
"url": "https://github.com/fabpot",
|
||||
"type": "github"
|
||||
},
|
||||
{
|
||||
"url": "https://github.com/nicolas-grekas",
|
||||
"type": "github"
|
||||
},
|
||||
{
|
||||
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
|
||||
"type": "tidelift"
|
||||
}
|
||||
],
|
||||
"time": "2026-08-25T14:18:37+00:00"
|
||||
},
|
||||
{
|
||||
"name": "symfony/deprecation-contracts",
|
||||
"version": "v3.7.1",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/symfony/deprecation-contracts.git",
|
||||
"reference": "f3202fa1b5097b0af062dc978b32ecf63404e31d"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/symfony/deprecation-contracts/zipball/f3202fa1b5097b0af062dc978b32ecf63404e31d",
|
||||
"reference": "f3202fa1b5097b0af062dc978b32ecf63404e31d",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"php": ">=8.1"
|
||||
},
|
||||
"type": "library",
|
||||
"extra": {
|
||||
"thanks": {
|
||||
"url": "https://github.com/symfony/contracts",
|
||||
"name": "symfony/contracts"
|
||||
},
|
||||
"branch-alias": {
|
||||
"dev-main": "3.7-dev"
|
||||
}
|
||||
},
|
||||
"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/v3.7.1"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
"url": "https://symfony.com/sponsor",
|
||||
"type": "custom"
|
||||
},
|
||||
{
|
||||
"url": "https://github.com/fabpot",
|
||||
"type": "github"
|
||||
},
|
||||
{
|
||||
"url": "https://github.com/nicolas-grekas",
|
||||
"type": "github"
|
||||
},
|
||||
{
|
||||
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
|
||||
"type": "tidelift"
|
||||
}
|
||||
],
|
||||
"time": "2026-06-05T06:23:12+00:00"
|
||||
},
|
||||
{
|
||||
"name": "symfony/polyfill-ctype",
|
||||
"version": "v1.37.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/symfony/polyfill-ctype.git",
|
||||
"reference": "141046a8f9477948ff284fa65be2095baafb94f2"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/141046a8f9477948ff284fa65be2095baafb94f2",
|
||||
"reference": "141046a8f9477948ff284fa65be2095baafb94f2",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"php": ">=7.2"
|
||||
},
|
||||
"provide": {
|
||||
"ext-ctype": "*"
|
||||
},
|
||||
"suggest": {
|
||||
"ext-ctype": "For best performance"
|
||||
},
|
||||
"type": "library",
|
||||
"extra": {
|
||||
"thanks": {
|
||||
"url": "https://github.com/symfony/polyfill",
|
||||
"name": "symfony/polyfill"
|
||||
}
|
||||
},
|
||||
"autoload": {
|
||||
"files": [
|
||||
"bootstrap.php"
|
||||
],
|
||||
"psr-4": {
|
||||
"Symfony\\Polyfill\\Ctype\\": ""
|
||||
}
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"MIT"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "Gert de Pagter",
|
||||
"email": "BackEndTea@gmail.com"
|
||||
},
|
||||
{
|
||||
"name": "Symfony Community",
|
||||
"homepage": "https://symfony.com/contributors"
|
||||
}
|
||||
],
|
||||
"description": "Symfony polyfill for ctype functions",
|
||||
"homepage": "https://symfony.com",
|
||||
"keywords": [
|
||||
"compatibility",
|
||||
"ctype",
|
||||
"polyfill",
|
||||
"portable"
|
||||
],
|
||||
"support": {
|
||||
"source": "https://github.com/symfony/polyfill-ctype/tree/v1.37.0"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
"url": "https://symfony.com/sponsor",
|
||||
"type": "custom"
|
||||
},
|
||||
{
|
||||
"url": "https://github.com/fabpot",
|
||||
"type": "github"
|
||||
},
|
||||
{
|
||||
"url": "https://github.com/nicolas-grekas",
|
||||
"type": "github"
|
||||
},
|
||||
{
|
||||
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
|
||||
"type": "tidelift"
|
||||
}
|
||||
],
|
||||
"time": "2026-04-10T16:19:22+00:00"
|
||||
},
|
||||
{
|
||||
"name": "symfony/polyfill-intl-grapheme",
|
||||
"version": "v1.41.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/symfony/polyfill-intl-grapheme.git",
|
||||
"reference": "bb899c1db0aa8127dc3afe8cda4a67eb24915f8d"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/symfony/polyfill-intl-grapheme/zipball/bb899c1db0aa8127dc3afe8cda4a67eb24915f8d",
|
||||
"reference": "bb899c1db0aa8127dc3afe8cda4a67eb24915f8d",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"php": ">=7.2"
|
||||
},
|
||||
"suggest": {
|
||||
"ext-intl": "For best performance"
|
||||
},
|
||||
"type": "library",
|
||||
"extra": {
|
||||
"thanks": {
|
||||
"url": "https://github.com/symfony/polyfill",
|
||||
"name": "symfony/polyfill"
|
||||
}
|
||||
},
|
||||
"autoload": {
|
||||
"files": [
|
||||
"bootstrap.php"
|
||||
],
|
||||
"psr-4": {
|
||||
"Symfony\\Polyfill\\Intl\\Grapheme\\": ""
|
||||
}
|
||||
},
|
||||
"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": "Symfony polyfill for intl's grapheme_* functions",
|
||||
"homepage": "https://symfony.com",
|
||||
"keywords": [
|
||||
"compatibility",
|
||||
"grapheme",
|
||||
"intl",
|
||||
"polyfill",
|
||||
"portable",
|
||||
"shim"
|
||||
],
|
||||
"support": {
|
||||
"source": "https://github.com/symfony/polyfill-intl-grapheme/tree/v1.41.0"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
"url": "https://symfony.com/sponsor",
|
||||
"type": "custom"
|
||||
},
|
||||
{
|
||||
"url": "https://github.com/fabpot",
|
||||
"type": "github"
|
||||
},
|
||||
{
|
||||
"url": "https://github.com/nicolas-grekas",
|
||||
"type": "github"
|
||||
},
|
||||
{
|
||||
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
|
||||
"type": "tidelift"
|
||||
}
|
||||
],
|
||||
"time": "2026-07-28T08:25:59+00:00"
|
||||
},
|
||||
{
|
||||
"name": "symfony/polyfill-intl-normalizer",
|
||||
"version": "v1.42.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/symfony/polyfill-intl-normalizer.git",
|
||||
"reference": "aa20edea75bd9c48cfecc8360922e5a6e5c44502"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/symfony/polyfill-intl-normalizer/zipball/aa20edea75bd9c48cfecc8360922e5a6e5c44502",
|
||||
"reference": "aa20edea75bd9c48cfecc8360922e5a6e5c44502",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"php": ">=7.2"
|
||||
},
|
||||
"suggest": {
|
||||
"ext-intl": "For best performance"
|
||||
},
|
||||
"type": "library",
|
||||
"extra": {
|
||||
"thanks": {
|
||||
"url": "https://github.com/symfony/polyfill",
|
||||
"name": "symfony/polyfill"
|
||||
}
|
||||
},
|
||||
"autoload": {
|
||||
"files": [
|
||||
"bootstrap.php"
|
||||
],
|
||||
"psr-4": {
|
||||
"Symfony\\Polyfill\\Intl\\Normalizer\\": ""
|
||||
},
|
||||
"classmap": [
|
||||
"Resources/stubs"
|
||||
]
|
||||
},
|
||||
"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": "Symfony polyfill for intl's Normalizer class and related functions",
|
||||
"homepage": "https://symfony.com",
|
||||
"keywords": [
|
||||
"compatibility",
|
||||
"intl",
|
||||
"normalizer",
|
||||
"polyfill",
|
||||
"portable",
|
||||
"shim"
|
||||
],
|
||||
"support": {
|
||||
"source": "https://github.com/symfony/polyfill-intl-normalizer/tree/v1.42.0"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
"url": "https://symfony.com/sponsor",
|
||||
"type": "custom"
|
||||
},
|
||||
{
|
||||
"url": "https://github.com/fabpot",
|
||||
"type": "github"
|
||||
},
|
||||
{
|
||||
"url": "https://github.com/nicolas-grekas",
|
||||
"type": "github"
|
||||
},
|
||||
{
|
||||
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
|
||||
"type": "tidelift"
|
||||
}
|
||||
],
|
||||
"time": "2026-08-07T06:33:24+00:00"
|
||||
},
|
||||
{
|
||||
"name": "symfony/polyfill-mbstring",
|
||||
"version": "v1.38.2",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/symfony/polyfill-mbstring.git",
|
||||
"reference": "d3d318bad5e7a1bfbd026009c8bfb8d8f99ae6b6"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/d3d318bad5e7a1bfbd026009c8bfb8d8f99ae6b6",
|
||||
"reference": "d3d318bad5e7a1bfbd026009c8bfb8d8f99ae6b6",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"ext-iconv": "*",
|
||||
"php": ">=7.2"
|
||||
},
|
||||
"provide": {
|
||||
"ext-mbstring": "*"
|
||||
},
|
||||
"suggest": {
|
||||
"ext-mbstring": "For best performance"
|
||||
},
|
||||
"type": "library",
|
||||
"extra": {
|
||||
"thanks": {
|
||||
"url": "https://github.com/symfony/polyfill",
|
||||
"name": "symfony/polyfill"
|
||||
}
|
||||
},
|
||||
"autoload": {
|
||||
"files": [
|
||||
"bootstrap.php"
|
||||
],
|
||||
"psr-4": {
|
||||
"Symfony\\Polyfill\\Mbstring\\": ""
|
||||
}
|
||||
},
|
||||
"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": "Symfony polyfill for the Mbstring extension",
|
||||
"homepage": "https://symfony.com",
|
||||
"keywords": [
|
||||
"compatibility",
|
||||
"mbstring",
|
||||
"polyfill",
|
||||
"portable",
|
||||
"shim"
|
||||
],
|
||||
"support": {
|
||||
"source": "https://github.com/symfony/polyfill-mbstring/tree/v1.38.2"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
"url": "https://symfony.com/sponsor",
|
||||
"type": "custom"
|
||||
},
|
||||
{
|
||||
"url": "https://github.com/fabpot",
|
||||
"type": "github"
|
||||
},
|
||||
{
|
||||
"url": "https://github.com/nicolas-grekas",
|
||||
"type": "github"
|
||||
},
|
||||
{
|
||||
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
|
||||
"type": "tidelift"
|
||||
}
|
||||
],
|
||||
"time": "2026-05-27T06:59:30+00:00"
|
||||
},
|
||||
{
|
||||
"name": "symfony/service-contracts",
|
||||
"version": "v3.7.3",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/symfony/service-contracts.git",
|
||||
"reference": "15e6a07ec2a2c75ceb1b21dd98105ee8456d2257"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/symfony/service-contracts/zipball/15e6a07ec2a2c75ceb1b21dd98105ee8456d2257",
|
||||
"reference": "15e6a07ec2a2c75ceb1b21dd98105ee8456d2257",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"php": ">=8.1",
|
||||
"psr/container": "^1.1|^2.0",
|
||||
"symfony/deprecation-contracts": "^2.5|^3"
|
||||
},
|
||||
"conflict": {
|
||||
"ext-psr": "<1.1|>=2"
|
||||
},
|
||||
"type": "library",
|
||||
"extra": {
|
||||
"thanks": {
|
||||
"url": "https://github.com/symfony/contracts",
|
||||
"name": "symfony/contracts"
|
||||
},
|
||||
"branch-alias": {
|
||||
"dev-main": "3.7-dev"
|
||||
}
|
||||
},
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"Symfony\\Contracts\\Service\\": ""
|
||||
},
|
||||
"exclude-from-classmap": [
|
||||
"/Test/"
|
||||
]
|
||||
},
|
||||
"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": "Generic abstractions related to writing services",
|
||||
"homepage": "https://symfony.com",
|
||||
"keywords": [
|
||||
"abstractions",
|
||||
"contracts",
|
||||
"decoupling",
|
||||
"interfaces",
|
||||
"interoperability",
|
||||
"standards"
|
||||
],
|
||||
"support": {
|
||||
"source": "https://github.com/symfony/service-contracts/tree/v3.7.3"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
"url": "https://symfony.com/sponsor",
|
||||
"type": "custom"
|
||||
},
|
||||
{
|
||||
"url": "https://github.com/fabpot",
|
||||
"type": "github"
|
||||
},
|
||||
{
|
||||
"url": "https://github.com/nicolas-grekas",
|
||||
"type": "github"
|
||||
},
|
||||
{
|
||||
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
|
||||
"type": "tidelift"
|
||||
}
|
||||
],
|
||||
"time": "2026-07-27T15:39:01+00:00"
|
||||
},
|
||||
{
|
||||
"name": "symfony/string",
|
||||
"version": "v7.4.15",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/symfony/string.git",
|
||||
"reference": "e394af32256bf9e7bf80849d95e589167c10097b"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/symfony/string/zipball/e394af32256bf9e7bf80849d95e589167c10097b",
|
||||
"reference": "e394af32256bf9e7bf80849d95e589167c10097b",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"php": ">=8.2",
|
||||
"symfony/deprecation-contracts": "^2.5|^3.0",
|
||||
"symfony/polyfill-ctype": "~1.8",
|
||||
"symfony/polyfill-intl-grapheme": "~1.33",
|
||||
"symfony/polyfill-intl-normalizer": "~1.0",
|
||||
"symfony/polyfill-mbstring": "~1.0"
|
||||
},
|
||||
"conflict": {
|
||||
"symfony/translation-contracts": "<2.5"
|
||||
},
|
||||
"require-dev": {
|
||||
"symfony/emoji": "^7.1|^8.0",
|
||||
"symfony/http-client": "^6.4|^7.0|^8.0",
|
||||
"symfony/intl": "^6.4|^7.0|^8.0",
|
||||
"symfony/translation-contracts": "^2.5|^3.0",
|
||||
"symfony/var-exporter": "^6.4|^7.0|^8.0"
|
||||
},
|
||||
"type": "library",
|
||||
"autoload": {
|
||||
"files": [
|
||||
"Resources/functions.php"
|
||||
],
|
||||
"psr-4": {
|
||||
"Symfony\\Component\\String\\": ""
|
||||
},
|
||||
"exclude-from-classmap": [
|
||||
"/Tests/"
|
||||
]
|
||||
},
|
||||
"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": "Provides an object-oriented API to strings and deals with bytes, UTF-8 code points and grapheme clusters in a unified way",
|
||||
"homepage": "https://symfony.com",
|
||||
"keywords": [
|
||||
"grapheme",
|
||||
"i18n",
|
||||
"string",
|
||||
"unicode",
|
||||
"utf-8",
|
||||
"utf8"
|
||||
],
|
||||
"support": {
|
||||
"source": "https://github.com/symfony/string/tree/v7.4.15"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
"url": "https://symfony.com/sponsor",
|
||||
"type": "custom"
|
||||
},
|
||||
{
|
||||
"url": "https://github.com/fabpot",
|
||||
"type": "github"
|
||||
},
|
||||
{
|
||||
"url": "https://github.com/nicolas-grekas",
|
||||
"type": "github"
|
||||
},
|
||||
{
|
||||
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
|
||||
"type": "tidelift"
|
||||
}
|
||||
],
|
||||
"time": "2026-07-28T07:33:02+00:00"
|
||||
}
|
||||
],
|
||||
"packages-dev": [],
|
||||
"aliases": [],
|
||||
"minimum-stability": "stable",
|
||||
"stability-flags": {},
|
||||
"prefer-stable": false,
|
||||
"prefer-lowest": false,
|
||||
"platform": {
|
||||
"php": ">=8.3",
|
||||
"ext-curl": "*",
|
||||
"ext-dom": "*",
|
||||
"ext-libxml": "*"
|
||||
},
|
||||
"platform-dev": {},
|
||||
"plugin-api-version": "2.6.0"
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace KTXM\ServiceWopi\Console;
|
||||
|
||||
use KTXM\ServiceWopi\Discovery\DiscoveryClient;
|
||||
use KTXM\ServiceWopi\Discovery\ServerAddress;
|
||||
use Symfony\Component\Console\Attribute\AsCommand;
|
||||
use Symfony\Component\Console\Command\Command;
|
||||
use Symfony\Component\Console\Input\InputArgument;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
use Symfony\Component\Console\Style\SymfonyStyle;
|
||||
use Throwable;
|
||||
|
||||
#[AsCommand(name: 'wopi:check', description: 'Check document server discovery without sending documents or credentials')]
|
||||
class CheckCommand extends Command
|
||||
{
|
||||
public function __construct(private readonly DiscoveryClient $client) { parent::__construct(); }
|
||||
|
||||
protected function configure(): void
|
||||
{
|
||||
$this->addArgument('server', InputArgument::REQUIRED, 'HTTPS base URL of the document server');
|
||||
}
|
||||
|
||||
protected function execute(InputInterface $input, OutputInterface $output): int
|
||||
{
|
||||
$io = new SymfonyStyle($input, $output);
|
||||
try {
|
||||
$document = $this->client->fetch(new ServerAddress($input->getArgument('server')));
|
||||
$rows = [];
|
||||
$hasDocxView = false;
|
||||
foreach ($document->actions as $action) {
|
||||
$rows[] = [$action->extension, $action->name, implode(', ', $action->requirements) ?: 'none'];
|
||||
$hasDocxView = $hasDocxView || ($action->extension === 'docx'
|
||||
&& $action->name === 'view' && $action->supportedBy([]));
|
||||
}
|
||||
$io->table(['Extension', 'Server action', 'Additional host requirements'], $rows);
|
||||
$io->text('Proof key metadata: ' . ($document->hasProofKeys ? 'present (not yet validated)' : 'absent'));
|
||||
if (!$hasDocxView) {
|
||||
$io->error('No DOCX view action usable by a basic read-only host.');
|
||||
return Command::FAILURE;
|
||||
}
|
||||
$io->success('Discovery supports the planned DOCX viewing flow.');
|
||||
$io->note('This check does not verify editor loading, WOPI callbacks, proof signatures, or saving.');
|
||||
return Command::SUCCESS;
|
||||
} catch (Throwable $error) {
|
||||
$io->error($error->getMessage());
|
||||
return Command::FAILURE;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace KTXM\ServiceWopi\Discovery;
|
||||
|
||||
final readonly class Action
|
||||
{
|
||||
/** @param list<string> $requirements */
|
||||
public function __construct(
|
||||
public string $extension,
|
||||
public string $name,
|
||||
public string $url,
|
||||
public array $requirements,
|
||||
) {}
|
||||
|
||||
/** @param list<string> $capabilities */
|
||||
public function supportedBy(array $capabilities): bool
|
||||
{
|
||||
$required = $this->requirements;
|
||||
if (in_array($this->name, ['edit', 'editnew', 'convert'], true)) {
|
||||
$required = array_merge($required, ['locks', 'update']);
|
||||
}
|
||||
return array_diff($required, $capabilities) === [];
|
||||
}
|
||||
|
||||
public function launchUrl(string $wopiSource): string
|
||||
{
|
||||
new ServerAddress($wopiSource);
|
||||
$url = preg_replace_callback('/<([^=<>]+)=([^<>]*?)(\&?)>/',
|
||||
static fn (array $match): string => $match[2] === 'WOPI_SOURCE'
|
||||
? $match[1] . '=' . rawurlencode($wopiSource) . $match[3] : '',
|
||||
$this->url,
|
||||
);
|
||||
$query = parse_url($url, PHP_URL_QUERY) ?? '';
|
||||
if (!preg_match('/(?:^|&)WOPISrc=/i', $query)) {
|
||||
$url = rtrim($url, '?&');
|
||||
$url .= (str_contains($url, '?') ? '&' : '?') . 'WOPISrc=' . rawurlencode($wopiSource);
|
||||
}
|
||||
return $url;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace KTXM\ServiceWopi\Discovery;
|
||||
|
||||
use RuntimeException;
|
||||
|
||||
class DiscoveryClient
|
||||
{
|
||||
public function fetch(ServerAddress $server): DiscoveryDocument
|
||||
{
|
||||
$curl = curl_init($server->discoveryUrl());
|
||||
$xml = '';
|
||||
curl_setopt_array($curl, [
|
||||
CURLOPT_CONNECTTIMEOUT => 5,
|
||||
CURLOPT_TIMEOUT => 15,
|
||||
CURLOPT_FOLLOWLOCATION => false,
|
||||
CURLOPT_PROTOCOLS => CURLPROTO_HTTPS,
|
||||
CURLOPT_SSL_VERIFYPEER => true,
|
||||
CURLOPT_SSL_VERIFYHOST => 2,
|
||||
CURLOPT_HTTPHEADER => ['Accept: application/xml, text/xml'],
|
||||
CURLOPT_WRITEFUNCTION => static function ($handle, string $chunk) use (&$xml): int {
|
||||
if (strlen($xml) + strlen($chunk) > DiscoveryDocument::MAX_BYTES) { return 0; }
|
||||
$xml .= $chunk;
|
||||
return strlen($chunk);
|
||||
},
|
||||
]);
|
||||
try {
|
||||
$ok = curl_exec($curl);
|
||||
$status = curl_getinfo($curl, CURLINFO_RESPONSE_CODE);
|
||||
if ($ok === false) {
|
||||
throw new RuntimeException('Discovery request failed (cURL ' . curl_errno($curl) . '). Check DNS, TLS, connectivity, and response size.');
|
||||
}
|
||||
if ($status !== 200) {
|
||||
throw new RuntimeException('Discovery returned HTTP ' . $status
|
||||
. ($status === 404 ? '. Enable WOPI and forward /hosting/discovery through the proxy.' : '. Expected HTTP 200; redirects are not followed.'));
|
||||
}
|
||||
return DiscoveryDocument::parse($xml, $server);
|
||||
} finally {
|
||||
curl_close($curl);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace KTXM\ServiceWopi\Discovery;
|
||||
|
||||
use DOMDocument;
|
||||
use DOMElement;
|
||||
use DOMXPath;
|
||||
use RuntimeException;
|
||||
|
||||
final readonly class DiscoveryDocument
|
||||
{
|
||||
public const MAX_BYTES = 2 * 1024 * 1024;
|
||||
|
||||
/** @param list<Action> $actions */
|
||||
private function __construct(public array $actions, public bool $hasProofKeys) {}
|
||||
|
||||
public static function parse(string $xml, ServerAddress $server): self
|
||||
{
|
||||
if ($xml === '' || strlen($xml) > self::MAX_BYTES || preg_match('/<!\s*(DOCTYPE|ENTITY)/i', $xml)) {
|
||||
throw new RuntimeException('Discovery XML is empty, oversized, or contains a forbidden document type/entity.');
|
||||
}
|
||||
$previous = libxml_use_internal_errors(true);
|
||||
try {
|
||||
$document = new DOMDocument();
|
||||
if (!$document->loadXML($xml, LIBXML_NONET | LIBXML_NOBLANKS)
|
||||
|| $document->doctype !== null || $document->documentElement?->nodeName !== 'wopi-discovery') {
|
||||
throw new RuntimeException('The server did not return valid WOPI discovery XML.');
|
||||
}
|
||||
$xpath = new DOMXPath($document);
|
||||
$nodes = $xpath->query('/wopi-discovery/net-zone[@name="external-https"]/app/action');
|
||||
// Euro-Office behind TLS termination can retain this zone label while
|
||||
// advertising HTTPS action URLs. Validate every URL independently.
|
||||
if ($nodes->length === 0) {
|
||||
$nodes = $xpath->query('/wopi-discovery/net-zone[@name="external-http"]/app/action');
|
||||
}
|
||||
$actions = [];
|
||||
foreach ($nodes as $node) {
|
||||
if (!$node instanceof DOMElement) { continue; }
|
||||
$name = $node->getAttribute('name');
|
||||
$extension = strtolower($node->getAttribute('ext'));
|
||||
if (!in_array($name, ['view', 'edit'], true) || !preg_match('/^[a-z0-9]+$/D', $extension)) {
|
||||
continue;
|
||||
}
|
||||
$url = $node->getAttribute('urlsrc');
|
||||
$server->assertTrustedAction($url);
|
||||
$requirements = preg_split('/\s*,\s*/', trim($node->getAttribute('requires')), -1, PREG_SPLIT_NO_EMPTY);
|
||||
$actions[] = new Action($extension, $name, $url, $requirements);
|
||||
}
|
||||
if ($actions === []) {
|
||||
throw new RuntimeException('Discovery has no external view/edit actions. Check WOPI enablement and forwarded HTTPS headers.');
|
||||
}
|
||||
$proof = $xpath->query('/wopi-discovery/proof-key')->item(0);
|
||||
return new self($actions, $proof instanceof DOMElement
|
||||
&& $proof->getAttribute('modulus') !== '' && $proof->getAttribute('exponent') !== '');
|
||||
} finally {
|
||||
libxml_clear_errors();
|
||||
libxml_use_internal_errors($previous);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace KTXM\ServiceWopi\Discovery;
|
||||
|
||||
use InvalidArgumentException;
|
||||
|
||||
final readonly class ServerAddress
|
||||
{
|
||||
public string $url;
|
||||
public string $origin;
|
||||
|
||||
public function __construct(string $url)
|
||||
{
|
||||
$parts = parse_url($url);
|
||||
if ($parts === false || strtolower($parts['scheme'] ?? '') !== 'https'
|
||||
|| empty($parts['host']) || isset($parts['user']) || isset($parts['pass'])
|
||||
|| isset($parts['query']) || isset($parts['fragment'])
|
||||
|| preg_match('/[\x00-\x20\x7f\\\\]/', $url)
|
||||
|| filter_var($url, FILTER_VALIDATE_URL) === false) {
|
||||
throw new InvalidArgumentException('Use an HTTPS server URL without credentials, query parameters, or a fragment.');
|
||||
}
|
||||
$this->origin = 'https://' . strtolower($parts['host'])
|
||||
. (isset($parts['port']) && $parts['port'] !== 443 ? ':' . $parts['port'] : '');
|
||||
$this->url = $this->origin . rtrim($parts['path'] ?? '', '/');
|
||||
}
|
||||
|
||||
public function discoveryUrl(): string { return $this->url . '/hosting/discovery'; }
|
||||
|
||||
public function assertTrustedAction(string $url): void
|
||||
{
|
||||
$plain = preg_replace('/<[^<>]*>/', '', $url);
|
||||
$parts = parse_url($plain);
|
||||
if ($parts === false || isset($parts['user']) || isset($parts['pass'])
|
||||
|| isset($parts['fragment']) || preg_match('/[\x00-\x20\x7f\\\\<>]/', $plain)
|
||||
|| filter_var($plain, FILTER_VALIDATE_URL) === false) {
|
||||
throw new InvalidArgumentException('Discovery contains an invalid action URL.');
|
||||
}
|
||||
$origin = strtolower($parts['scheme'] ?? '') . '://' . strtolower($parts['host'] ?? '')
|
||||
. (isset($parts['port']) && $parts['port'] !== 443 ? ':' . $parts['port'] : '');
|
||||
if ($origin !== $this->origin) {
|
||||
throw new InvalidArgumentException('Discovery action URL is outside the configured HTTPS server origin.');
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace KTXM\ServiceWopi;
|
||||
|
||||
use KTXF\Module\Configuration\ConsoleModuleContextInterface;
|
||||
use KTXF\Module\Configuration\ModuleContextInterface;
|
||||
use KTXF\Module\ModuleInstanceAbstract;
|
||||
use KTXM\ServiceWopi\Console\CheckCommand;
|
||||
|
||||
class Module extends ModuleInstanceAbstract
|
||||
{
|
||||
public function handle(): string { return 'service_wopi'; }
|
||||
public function label(): string { return 'WOPI Service'; }
|
||||
public function author(): string { return 'Ktrix'; }
|
||||
public function version(): string { return '0.0.1'; }
|
||||
public function description(): string { return 'WOPI discovery and connection diagnostics'; }
|
||||
|
||||
public function configure(ModuleContextInterface $context): void
|
||||
{
|
||||
if ($context instanceof ConsoleModuleContextInterface) {
|
||||
$context->registerCommand(CheckCommand::class);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,151 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use KTXM\ServiceWopi\Console\CheckCommand;
|
||||
use KTXM\ServiceWopi\Discovery\Action;
|
||||
use KTXM\ServiceWopi\Discovery\DiscoveryClient;
|
||||
use KTXM\ServiceWopi\Discovery\DiscoveryDocument;
|
||||
use KTXM\ServiceWopi\Discovery\ServerAddress;
|
||||
use PHPUnit\Framework\Attributes\DataProvider;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Symfony\Component\Console\Tester\CommandTester;
|
||||
|
||||
final class DiscoveryTest extends TestCase
|
||||
{
|
||||
private function xml(string $action = 'view', string $url = 'https://office.test/editor?', string $requires = ''): string
|
||||
{
|
||||
return '<wopi-discovery><net-zone name="external-https"><app name="Word">'
|
||||
. '<action ext="docx" name="' . $action . '" requires="' . $requires
|
||||
. '" urlsrc="' . htmlspecialchars($url, ENT_XML1 | ENT_QUOTES) . '"/>'
|
||||
. '</app></net-zone><proof-key modulus="abc" exponent="AQAB"/></wopi-discovery>';
|
||||
}
|
||||
|
||||
public function testParsesActionsAndDoesNotPermitEditingWithoutHostCapabilities(): void
|
||||
{
|
||||
$server = new ServerAddress('https://OFFICE.test:443/');
|
||||
self::assertSame('https://office.test/hosting/discovery', $server->discoveryUrl());
|
||||
$document = DiscoveryDocument::parse($this->xml(), $server);
|
||||
self::assertTrue($document->hasProofKeys);
|
||||
self::assertTrue($document->actions[0]->supportedBy([]));
|
||||
$edit = DiscoveryDocument::parse($this->xml('edit'), $server)->actions[0];
|
||||
self::assertFalse($edit->supportedBy([]));
|
||||
self::assertFalse($edit->supportedBy(['locks']));
|
||||
self::assertTrue($edit->supportedBy(['locks', 'update']));
|
||||
$extra = DiscoveryDocument::parse($this->xml('view', requires: ' containers, future '), $server)->actions[0];
|
||||
self::assertFalse($extra->supportedBy([]));
|
||||
}
|
||||
|
||||
public function testAcceptsEuroOfficeExternalHttpLabelOnlyWithTrustedHttpsUrls(): void
|
||||
{
|
||||
$xml = str_replace('external-https', 'external-http', $this->xml(
|
||||
url: 'https://office.test/editor?<ui=UI_LLCC&><wopisrc=WOPI_SOURCE&>&',
|
||||
));
|
||||
$document = DiscoveryDocument::parse($xml, new ServerAddress('https://office.test'));
|
||||
$source = 'https://app.test/wopi/files/123';
|
||||
parse_str(parse_url($document->actions[0]->launchUrl($source), PHP_URL_QUERY), $parameters);
|
||||
self::assertSame(['wopisrc' => $source], $parameters);
|
||||
|
||||
$this->expectException(InvalidArgumentException::class);
|
||||
DiscoveryDocument::parse(str_replace('https://office.test', 'http://office.test', $xml), new ServerAddress('https://office.test'));
|
||||
}
|
||||
|
||||
#[DataProvider('invalidServers')]
|
||||
public function testRejectsInvalidServerAddresses(string $url): void
|
||||
{
|
||||
$this->expectException(InvalidArgumentException::class);
|
||||
new ServerAddress($url);
|
||||
}
|
||||
|
||||
public static function invalidServers(): array
|
||||
{
|
||||
return array_map(static fn ($url) => [$url], [
|
||||
'http://office.test', 'file:///etc/passwd', 'https://user:pass@office.test',
|
||||
'https://office.test?secret=x', 'https://office.test/#fragment',
|
||||
"https://office.test/\r\n", 'https://office.test\\@other.test',
|
||||
]);
|
||||
}
|
||||
|
||||
#[DataProvider('invalidDocuments')]
|
||||
public function testRejectsInvalidDiscovery(string $xml): void
|
||||
{
|
||||
$this->expectException(RuntimeException::class);
|
||||
DiscoveryDocument::parse($xml, new ServerAddress('https://office.test'));
|
||||
}
|
||||
|
||||
public static function invalidDocuments(): array
|
||||
{
|
||||
return [
|
||||
[''], ['<html><body>Welcome</body></html>'], ['<wopi-discovery>'],
|
||||
['<!DOCTYPE wopi-discovery [<!ENTITY x SYSTEM "file:///etc/passwd">]><wopi-discovery>&x;</wopi-discovery>'],
|
||||
['<wopi-discovery><net-zone name="external-http"><app/></net-zone></wopi-discovery>'],
|
||||
[str_repeat('x', DiscoveryDocument::MAX_BYTES + 1)],
|
||||
];
|
||||
}
|
||||
|
||||
#[DataProvider('untrustedActions')]
|
||||
public function testRejectsUntrustedActions(string $url): void
|
||||
{
|
||||
$this->expectException(InvalidArgumentException::class);
|
||||
DiscoveryDocument::parse($this->xml(url: $url), new ServerAddress('https://office.test'));
|
||||
}
|
||||
|
||||
public static function untrustedActions(): array
|
||||
{
|
||||
return array_map(static fn ($url) => [$url], [
|
||||
'http://office.test/editor', 'https://evil.test/editor',
|
||||
'https://office.test.evil.test/editor', 'https://office.test:8443/editor',
|
||||
'https://user@office.test/editor', '//office.test/editor',
|
||||
'javascript:alert(1)', 'https://office.test/editor#fragment',
|
||||
]);
|
||||
}
|
||||
|
||||
#[DataProvider('launchTemplates')]
|
||||
public function testBuildsLaunchUrlWithoutTokenAndRemovesUnknownPlaceholders(string $template): void
|
||||
{
|
||||
$action = DiscoveryDocument::parse($this->xml(url: $template), new ServerAddress('https://office.test'))->actions[0];
|
||||
$source = 'https://app.test/m/service_wopi/files/opaque-id';
|
||||
$url = $action->launchUrl($source);
|
||||
self::assertStringNotContainsString('<', $url);
|
||||
self::assertStringNotContainsString('access_token', $url);
|
||||
parse_str(parse_url($url, PHP_URL_QUERY), $parameters);
|
||||
self::assertSame($source, $parameters['WOPISrc']);
|
||||
self::assertCount(1, $parameters);
|
||||
}
|
||||
|
||||
public static function launchTemplates(): array
|
||||
{
|
||||
return [
|
||||
['https://office.test/editor'],
|
||||
['https://office.test/editor?'],
|
||||
['https://office.test/editor?<ui=UI_LLCC&><future=UNKNOWN&>'],
|
||||
['https://office.test/editor?<WOPISrc=WOPI_SOURCE&><ui=UI_LLCC&>'],
|
||||
];
|
||||
}
|
||||
|
||||
public function testCommandReports404WithoutClaimingConnectionSuccess(): void
|
||||
{
|
||||
$client = $this->createStub(DiscoveryClient::class);
|
||||
$client->method('fetch')->willThrowException(new RuntimeException('Discovery returned HTTP 404.'));
|
||||
$command = new CommandTester(new CheckCommand($client));
|
||||
self::assertSame(1, $command->execute(['server' => 'https://office.test']));
|
||||
self::assertStringContainsString('HTTP 404', $command->getDisplay());
|
||||
}
|
||||
|
||||
public function testCommandQualifiesViewingOnly(): void
|
||||
{
|
||||
$client = $this->createStub(DiscoveryClient::class);
|
||||
$client->method('fetch')->willReturn(DiscoveryDocument::parse($this->xml(), new ServerAddress('https://office.test')));
|
||||
$command = new CommandTester(new CheckCommand($client));
|
||||
self::assertSame(0, $command->execute(['server' => 'https://office.test']));
|
||||
self::assertStringContainsString('does not verify', $command->getDisplay());
|
||||
}
|
||||
|
||||
public function testEditOnlyDiscoveryDoesNotPassReadOnlyQualification(): void
|
||||
{
|
||||
$client = $this->createStub(DiscoveryClient::class);
|
||||
$client->method('fetch')->willReturn(DiscoveryDocument::parse($this->xml('edit'), new ServerAddress('https://office.test')));
|
||||
$command = new CommandTester(new CheckCommand($client));
|
||||
self::assertSame(1, $command->execute(['server' => 'https://office.test']));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
<?php
|
||||
|
||||
$loader = require dirname(__DIR__, 4) . '/vendor/autoload.php';
|
||||
$loader->addPsr4('KTXM\\ServiceWopi\\', dirname(__DIR__, 2) . '/lib/');
|
||||
@@ -0,0 +1,4 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<phpunit bootstrap="bootstrap.php" failOnWarning="true" failOnNotice="true" failOnDeprecation="true" cacheDirectory="/tmp/ktrix-wopi-phpunit">
|
||||
<testsuites><testsuite name="WOPI"><directory>Unit</directory></testsuite></testsuites>
|
||||
</phpunit>
|
||||
Reference in New Issue
Block a user