├── psalm-baseline.xml ├── src ├── Contract │ ├── Entity │ │ └── Document.php │ └── Service │ │ ├── ProofValidatorInterface.php │ │ ├── Configuration │ │ └── ConfigurationInterface.php │ │ ├── Clock │ │ └── ClockInterface.php │ │ ├── Utils │ │ └── DotNetTimeConverterInterface.php │ │ ├── DocumentLockManagerInterface.php │ │ ├── Discovery │ │ └── DiscoveryInterface.php │ │ ├── DocumentManagerInterface.php │ │ └── WopiInterface.php └── Service │ ├── Clock │ └── SystemClock.php │ ├── Utils │ └── DotNetTimeConverter.php │ ├── Configuration │ └── Configuration.php │ ├── DocumentLockManager.php │ ├── ProofValidator.php │ └── Discovery │ └── Discovery.php ├── LICENSE ├── composer.json ├── README.md └── CHANGELOG.md /psalm-baseline.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /src/Contract/Entity/Document.php: -------------------------------------------------------------------------------- 1 | getTimestamp() * self::MULTIPLIER) + self::OFFSET); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2021-2022 Champs-Libres 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software is furnished to do so, 10 | subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 17 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 18 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 19 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /src/Service/Configuration/Configuration.php: -------------------------------------------------------------------------------- 1 | properties = $properties; 23 | } 24 | 25 | public function jsonSerialize(): array 26 | { 27 | return $this->properties; 28 | } 29 | 30 | public function offsetExists($offset): bool 31 | { 32 | return array_key_exists($offset, $this->properties); 33 | } 34 | 35 | public function offsetGet($offset): mixed 36 | { 37 | return $this->properties[$offset]; 38 | } 39 | 40 | public function offsetSet($offset, $value): void 41 | { 42 | $this->properties[$offset] = $value; 43 | } 44 | 45 | public function offsetUnset($offset): void 46 | { 47 | unset($this->properties[$offset]); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/Contract/Service/Discovery/DiscoveryInterface.php: -------------------------------------------------------------------------------- 1 | 16 | */ 17 | public function discoverAction(string $extension, string $name = 'view'): ?array; 18 | 19 | /** 20 | * @return list> 21 | */ 22 | public function discoverExtension(string $extension): array; 23 | 24 | /** 25 | * @return list> 26 | */ 27 | public function discoverMimeType(string $mimeType): array; 28 | 29 | /** 30 | * @return array 31 | */ 32 | public function getCapabilities(): array; 33 | 34 | /** 35 | * @return string The MSBLOB key is the "value" attribute in the proof-key tag in the hosting/capabilities xml 36 | */ 37 | public function getPublicKey(): string; 38 | 39 | /** 40 | * @return string The MSBLOB oldkey is the "oldvalue" attribute in the proof-key tag in the hosting/capabilities xml 41 | */ 42 | public function getPublicKeyOld(): string; 43 | } 44 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "champs-libres/wopi-lib", 3 | "description": "A standard and framework agnostic PHP library to facilitate the implementation of the WOPI protocol.", 4 | "license": "MIT", 5 | "keywords": [ 6 | "wopi" 7 | ], 8 | "homepage": "http://github.com/champs-libres/wopi-lib", 9 | "support": { 10 | "issues": "https://github.com/champs-libres/wopi-lib/issues", 11 | "source": "https://github.com/champs-libres/wopi-lib", 12 | "docs": "https://github.com/champs-libres/wopi-lib" 13 | }, 14 | "minimum-stability": "stable", 15 | "prefer-stable": true, 16 | "require": { 17 | "php": ">= 7.4", 18 | "ext-SimpleXML": "*", 19 | "ext-json": "*", 20 | "ext-openssl": "*", 21 | "loophp/psr17": "^1.0", 22 | "phpseclib/phpseclib": "^3.0", 23 | "psr/cache": "^1.0 || ^2.0 || ^3.0", 24 | "psr/http-client": "^1.0", 25 | "psr/http-client-implementation": "^1", 26 | "psr/http-factory": "^1.0.1", 27 | "psr/http-factory-implementation": "^1", 28 | "psr/http-message": "^1.0 || ^2.0", 29 | "psr/http-message-implementation": "^1" 30 | }, 31 | "require-dev": { 32 | "drupol/php-conventions": "^5.0", 33 | "friends-of-phpspec/phpspec-code-coverage": "^6.1", 34 | "nyholm/psr7": "^1.4", 35 | "phpspec/phpspec": "^7.1", 36 | "symfony/http-client": "^5.3" 37 | }, 38 | "autoload": { 39 | "psr-4": { 40 | "ChampsLibres\\WopiLib\\": "src/" 41 | } 42 | }, 43 | "config": { 44 | "allow-plugins": { 45 | "phpstan/extension-installer": true, 46 | "phpro/grumphp": true, 47 | "ergebnis/composer-normalize": true 48 | } 49 | }, 50 | "scripts": { 51 | "changelog-unreleased": "docker-compose run auto_changelog -c .auto-changelog -u", 52 | "changelog-version": "docker-compose run auto_changelog -c .auto-changelog -v", 53 | "grumphp": "./vendor/bin/grumphp run", 54 | "infection": "vendor/bin/infection run -j 2", 55 | "phpspec": "vendor/bin/phpspec run -vvv --stop-on-failure" 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/Service/DocumentLockManager.php: -------------------------------------------------------------------------------- 1 | cache = $cache; 25 | } 26 | 27 | public function deleteLock(Document $document, RequestInterface $request): bool 28 | { 29 | // instead of deleting the lock, set a short expiration time 30 | // it gives a chance for concurrent request which put the file content 31 | // to meet an existing lock, and avoid them to be banned 32 | $item = $this->cache->getItem($this->getCacheId($document->getWopiDocId())); 33 | 34 | $item->expiresAfter(new DateInterval('PT10S')); 35 | 36 | return $this->cache->save($item); 37 | } 38 | 39 | public function getLock(Document $document, RequestInterface $request): string 40 | { 41 | return $this->cache->getItem($this->getCacheId($document->getWopiDocId()))->get(); 42 | } 43 | 44 | public function hasLock(Document $document, RequestInterface $request): bool 45 | { 46 | $item = $this->cache->getItem($this->getCacheId($document->getWopiDocId())); 47 | 48 | return $item->isHit(); 49 | } 50 | 51 | public function setLock(Document $document, string $lockId, RequestInterface $request): bool 52 | { 53 | $item = $this->cache->getItem($this->getCacheId($document->getWopiDocId())); 54 | 55 | $item->set($lockId); 56 | // according to the specs, lock should last 30M 57 | $item->expiresAfter(new DateInterval('PT31M')); 58 | 59 | return $this->cache->save($item); 60 | } 61 | 62 | private function getCacheId(string $documentId): string 63 | { 64 | return sprintf('wopi_lib_lock_%s', $documentId); 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /src/Service/ProofValidator.php: -------------------------------------------------------------------------------- 1 | clock = $clock; 38 | $this->discovery = $discovery; 39 | $this->dotNetTimeConverter = $dotNetTimeConverter; 40 | } 41 | 42 | public function isValid(RequestInterface $request): bool 43 | { 44 | $timestamp = $request->getHeaderLine(WopiInterface::HEADER_TIMESTAMP); 45 | $date = $this->dotNetTimeConverter->toDatetime($timestamp); 46 | 47 | if (20 * 60 < ($this->clock->now()->getTimestamp() - $date->getTimestamp())) { 48 | return false; 49 | } 50 | 51 | $params = []; 52 | parse_str($request->getUri()->getQuery(), $params); 53 | $url = (string) $request->getUri(); 54 | 55 | $expected = sprintf( 56 | '%s%s%s%s%s%s', 57 | pack('N', strlen($params['access_token'])), 58 | $params['access_token'], 59 | pack('N', strlen($url)), 60 | strtoupper($url), 61 | pack('N', 8), 62 | pack('J', $timestamp) 63 | ); 64 | 65 | $key = $this->discovery->getPublicKey(); 66 | $keyOld = $this->discovery->getPublicKeyOld(); 67 | $xWopiProof = $request->getHeaderLine(WopiInterface::HEADER_PROOF); 68 | $xWopiProofOld = $request->getHeaderLine(WopiInterface::HEADER_PROOF_OLD); 69 | 70 | return $this->verify($expected, $xWopiProof, $key) 71 | || $this->verify($expected, $xWopiProofOld, $key) 72 | || $this->verify($expected, $xWopiProof, $keyOld); 73 | } 74 | 75 | /** 76 | * @param string $key The key in MSBLOB format 77 | */ 78 | private function verify(string $expected, string $proof, string $key): bool 79 | { 80 | try { 81 | /** @var RSA $key */ 82 | $key = PublicKeyLoader::loadPublicKey($key); 83 | } catch (Throwable $e) { 84 | return false; 85 | } 86 | 87 | return $key 88 | ->withHash('sha256') 89 | ->withPadding(RSA::SIGNATURE_RELAXED_PKCS1) 90 | ->verify($expected, (string) base64_decode($proof, true)); 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /src/Contract/Service/DocumentManagerInterface.php: -------------------------------------------------------------------------------- 1 | configuration = $configuration; 36 | $this->client = $client; 37 | $this->psr17 = $psr17; 38 | } 39 | 40 | public function discoverAction(string $extension, string $name = 'view'): ?array 41 | { 42 | /** @var false|SimpleXMLElement[]|null $apps */ 43 | $apps = $this->discover()->xpath('//net-zone/app'); 44 | 45 | if (false === $apps || null === $apps) { 46 | throw new Exception(); 47 | } 48 | 49 | $return = []; 50 | 51 | foreach ($apps as $app) { 52 | /** @var false|SimpleXMLElement[]|null $actions */ 53 | $actions = $app->xpath(sprintf('action[@ext="%s" and @name="%s"]', $extension, $name)); 54 | 55 | if (false === $actions || null === $actions) { 56 | continue; 57 | } 58 | 59 | foreach ($actions as $action) { 60 | $actionAttributes = $action->attributes() ?: []; 61 | 62 | $return[] = array_merge( 63 | (array) reset($actionAttributes), 64 | ['app' => (string) $app['name']], 65 | ['favIconUrl' => (string) $app['favIconUrl']] 66 | ); 67 | } 68 | } 69 | 70 | return (false === $action = current($return)) ? null : $action; 71 | } 72 | 73 | public function discoverExtension(string $extension): array 74 | { 75 | $extensions = []; 76 | 77 | /** @var false|SimpleXMLElement[]|null $apps */ 78 | $apps = $this->discover()->xpath('//net-zone/app'); 79 | 80 | if (false === $apps || null === $apps) { 81 | throw new Exception(); 82 | } 83 | 84 | foreach ($apps as $app) { 85 | /** @var false|SimpleXMLElement[]|null $actions */ 86 | $actions = $app->xpath(sprintf("action[@ext='%s']", $extension)); 87 | 88 | if (false === $actions || null === $actions) { 89 | continue; 90 | } 91 | 92 | foreach ($actions as $action) { 93 | $actionAttributes = $action->attributes() ?: []; 94 | 95 | $extensions[] = array_merge( 96 | (array) reset($actionAttributes), 97 | ['name' => (string) $app['name']], 98 | ['favIconUrl' => (string) $app['favIconUrl']] 99 | ); 100 | } 101 | } 102 | 103 | return $extensions; 104 | } 105 | 106 | public function discoverMimeType(string $mimeType): array 107 | { 108 | $mimeTypes = []; 109 | 110 | /** @var false|SimpleXMLElement[]|null $apps */ 111 | $apps = $this->discover()->xpath(sprintf("//net-zone/app[@name='%s']", $mimeType)); 112 | 113 | if (false === $apps || null === $apps) { 114 | throw new Exception(); 115 | } 116 | 117 | foreach ($apps as $app) { 118 | /** @var false|SimpleXMLElement[]|null $actions */ 119 | $actions = $app->xpath('action'); 120 | 121 | if (false === $actions || null === $actions) { 122 | continue; 123 | } 124 | 125 | foreach ($actions as $action) { 126 | $actionAttributes = $action->attributes() ?: []; 127 | 128 | $mimeTypes[] = array_merge( 129 | (array) reset($actionAttributes), 130 | ['name' => (string) $app['name']], 131 | ); 132 | } 133 | } 134 | 135 | return $mimeTypes; 136 | } 137 | 138 | public function getCapabilities(): array 139 | { 140 | $capabilities = $this->discover()->xpath("//net-zone/app[@name='Capabilities']"); 141 | 142 | if (false === $capabilities = reset($capabilities)) { 143 | return []; 144 | } 145 | 146 | return json_decode( 147 | (string) $this->request((string) $capabilities->action['urlsrc'])->getBody(), 148 | true, 149 | 512, 150 | JSON_THROW_ON_ERROR 151 | ); 152 | } 153 | 154 | public function getPublicKey(): string 155 | { 156 | return (string) $this->discover()->xpath('//proof-key/@value')[0]; 157 | } 158 | 159 | public function getPublicKeyOld(): string 160 | { 161 | return (string) $this->discover()->xpath('//proof-key/@oldvalue')[0]; 162 | } 163 | 164 | private function discover(): SimpleXMLElement 165 | { 166 | $simpleXmlElement = simplexml_load_string( 167 | (string) $this 168 | ->request( 169 | sprintf('%s/%s', $this->configuration['server'], 'hosting/discovery') 170 | ) 171 | ->getBody() 172 | ); 173 | 174 | if (false === $simpleXmlElement) { 175 | // TODO 176 | throw new Exception('Unable to parse XML.'); 177 | } 178 | 179 | return $simpleXmlElement; 180 | } 181 | 182 | private function request(string $url): ResponseInterface 183 | { 184 | $response = $this 185 | ->client 186 | ->sendRequest($this->psr17->createRequest('GET', $url)); 187 | 188 | if (200 !== $response->getStatusCode()) { 189 | // TODO 190 | throw new Exception('Invalid status code'); 191 | } 192 | 193 | return $response; 194 | } 195 | } 196 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to this project will be documented in this file. 4 | 5 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) 6 | and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). 7 | 8 | ## [Unreleased](https://github.com/Champs-Libres/wopi-lib/compare/0.0.1...HEAD) 9 | 10 | ### Merged 11 | 12 | - chore(deps): Bump shivammathur/setup-php from 2.13.0 to 2.14.0 [`#2`](https://github.com/Champs-Libres/wopi-lib/pull/2) 13 | - chore(deps): Bump shivammathur/setup-php from 2.12.0 to 2.13.0 [`#1`](https://github.com/Champs-Libres/wopi-lib/pull/1) 14 | 15 | ### Commits 16 | 17 | - refactor: Add a `DotNetTimeConverter` and tests. [`2dea8ae`](https://github.com/Champs-Libres/wopi-lib/commit/2dea8aeede580fbba1e7e1af3384ba03220a0fe5) 18 | - refactor: Update/add new interfaces to provide more abstractions. [`709652f`](https://github.com/Champs-Libres/wopi-lib/commit/709652f208eb6483150e5dba93380819c2ffeea4) 19 | - refactor: Move things around and rename. [`fa75b24`](https://github.com/Champs-Libres/wopi-lib/commit/fa75b24f811ebfeae0675cdb53e6855b2c604389) 20 | - Fix parameter name, use camelcase. [`6cf16ee`](https://github.com/Champs-Libres/wopi-lib/commit/6cf16ee488b384ce7d63e2601d571bfb32fa8d28) 21 | - refactor: Update `WOPI::putRelativeFile' interface. [`7d9e6f2`](https://github.com/Champs-Libres/wopi-lib/commit/7d9e6f28cb26d39e597ee036b462be49e41a6470) 22 | - refactor: Use PHPSecLib only for verification. [`d026ffc`](https://github.com/Champs-Libres/wopi-lib/commit/d026ffce5eec607ba9575aae8d0fd7d9af3e0671) 23 | - refactor: Prevent issue when there are no capabilities. [`99f2340`](https://github.com/Champs-Libres/wopi-lib/commit/99f23406ef20f46c0f397d3d35f12b68516b1a89) 24 | - refactor: `WopiProofValidator` - Make sure timestamp is properly checked. [`f4e8b78`](https://github.com/Champs-Libres/wopi-lib/commit/f4e8b78bd3e9262ab0daf746a3310d5a5c69ebad) 25 | - refactor: Remove obsolete method. [`06d808b`](https://github.com/Champs-Libres/wopi-lib/commit/06d808b60d21b54b36aa3715a6c494140b856612) 26 | - Autofix code style. [`10440f8`](https://github.com/Champs-Libres/wopi-lib/commit/10440f8fad4a8c63e923516fb68af9d4eea236a6) 27 | - feat: Add `WopiProofValidator` service. [`8362e52`](https://github.com/Champs-Libres/wopi-lib/commit/8362e52260ff492458101c9a2badd054dcd2d0fe) 28 | - refactor: Update `WopiDiscovery::getPublicKey()` and add `WopiDiscovery::getPublicKeyOld()`. [`e993987`](https://github.com/Champs-Libres/wopi-lib/commit/e99398793d7f6671507ab30b75c5ccf5a469c099) 29 | - feat: Add `WopiDiscovery::getPublicKey()` - Update `WopiDiscoveryInterface`. [`52d862b`](https://github.com/Champs-Libres/wopi-lib/commit/52d862b46ff5605efafbb0e8865778365f9470c2) 30 | - fix: Make Grumphp happy. [`3afb336`](https://github.com/Champs-Libres/wopi-lib/commit/3afb336c5bdb00d56ebd21af2318c52f7db0fd4f) 31 | - feat: Add `WopiDiscovery::getPublicKey()`. [`806fc4e`](https://github.com/Champs-Libres/wopi-lib/commit/806fc4e42e634da4a4414d62b0ff486411d89709) 32 | - chore: Update psr/cache minimum version. [`9447aef`](https://github.com/Champs-Libres/wopi-lib/commit/9447aef99a7a63d4cc942ace89a497e88f09ad15) 33 | - chore: Update psr/cache minimum version. [`160995e`](https://github.com/Champs-Libres/wopi-lib/commit/160995eb49a5b459517456fed838fbd93109cf4b) 34 | - refactor: New interfaces and default implementation of a basic document lock manager. [`5e1887f`](https://github.com/Champs-Libres/wopi-lib/commit/5e1887f25d8b73f814a880ff0c263d2b56af6431) 35 | - tests: `WopiDiscovery` spec tests. [`192136a`](https://github.com/Champs-Libres/wopi-lib/commit/192136a9eb1fecdc1feb929d0b01c218ee65c7aa) 36 | - tests: `WopiConfiguration` spec tests. [`fa227cd`](https://github.com/Champs-Libres/wopi-lib/commit/fa227cddb337b9d26810e7ef6bd522d84e41ed5f) 37 | - refactor: Update return types. [`757d4ff`](https://github.com/Champs-Libres/wopi-lib/commit/757d4ff62d11fa59197bf783874b2d76d641a030) 38 | - tests: Disable some tests until phpspec/phpspec#1383 is fixed. [`dfb3266`](https://github.com/Champs-Libres/wopi-lib/commit/dfb3266745644089dc3bb00dd6ee94bf8c50fbd3) 39 | - Add WopiConfiguration class. [`3598fa7`](https://github.com/Champs-Libres/wopi-lib/commit/3598fa7b29406e6d0931d9faa47f489d1c9b5861) 40 | - refactor: Add mimetype discovery. [`d729ec6`](https://github.com/Champs-Libres/wopi-lib/commit/d729ec6169d626bcab19174dae4601c9da10a3ad) 41 | - refactor: Fix bug in WopiDiscovery. [`8d80604`](https://github.com/Champs-Libres/wopi-lib/commit/8d8060477969837ac07672c09b4fb37ff9487bd7) 42 | - refactor: Update Psalm configuration. [`f963bff`](https://github.com/Champs-Libres/wopi-lib/commit/f963bff57e32af6799021b62a01df40230858839) 43 | - refactor: Update static analysis documentation. [`ab455d0`](https://github.com/Champs-Libres/wopi-lib/commit/ab455d0be01c5ecc217b937894cad7713c175fd8) 44 | - chore: Update static files. [`9b0be73`](https://github.com/Champs-Libres/wopi-lib/commit/9b0be738be627ac68abfd56f8459cda234ebeb46) 45 | - docs: Update README. [`fcf7119`](https://github.com/Champs-Libres/wopi-lib/commit/fcf7119d164385c0fbe70604bd4c798e9479fb33) 46 | - refactor: Simplification. [`70d2a1c`](https://github.com/Champs-Libres/wopi-lib/commit/70d2a1c1f368b32acd1ff6f681a1ea6e65ae47a6) 47 | - refactor: Update interface. [`ae4ac85`](https://github.com/Champs-Libres/wopi-lib/commit/ae4ac85c1fb118b3433676d83e7783d952d698d9) 48 | - refactor: Remove cache layer. Should be implemented in the HTTP client. [`3affbf2`](https://github.com/Champs-Libres/wopi-lib/commit/3affbf21148ee294db30bba2323048e603ca52fb) 49 | - refactor: Autofix code style. [`50eaa46`](https://github.com/Champs-Libres/wopi-lib/commit/50eaa466ff6a32682fd7c9bc60436f48431fa93c) 50 | - chore: Normalize composer.json. [`8ff472e`](https://github.com/Champs-Libres/wopi-lib/commit/8ff472e5a88653b62e4fdf59359286c460407bb0) 51 | - refactor: Add favIconUrl. [`28f6935`](https://github.com/Champs-Libres/wopi-lib/commit/28f69354fecace721f1b8f3f9064b08b6c776ceb) 52 | - refactor: Autofix code style. [`a0a2275`](https://github.com/Champs-Libres/wopi-lib/commit/a0a227525d923009f4fba08e16f4b8d3c3dbac26) 53 | - refactor: Do not try to convert the XML into an array. [`666aa37`](https://github.com/Champs-Libres/wopi-lib/commit/666aa37149b1e2d45d9f074771995d2a4c3030ce) 54 | - refactor: Use XML2Array. [`577146c`](https://github.com/Champs-Libres/wopi-lib/commit/577146c81f8ffc309e3404358d620a0c9bad5ece) 55 | - refactor: Update class name. [`59c3d93`](https://github.com/Champs-Libres/wopi-lib/commit/59c3d93e28ec7c8fb772ccb69c4171306efbf390) 56 | - refactor: Update namespaces. [`bcd94e2`](https://github.com/Champs-Libres/wopi-lib/commit/bcd94e2886f61b325f5ba43664c07e7c44a082cc) 57 | - chore: Update composer.json. [`bad05c1`](https://github.com/Champs-Libres/wopi-lib/commit/bad05c16265b5e2765c72bdf4848d829618480a2) 58 | 59 | ## 0.0.1 - 2021-08-05 60 | 61 | ### Commits 62 | 63 | - Initial set of files. [`22333c1`](https://github.com/Champs-Libres/wopi-lib/commit/22333c18447bed681766a2b7f85da070ab5577cb) 64 | --------------------------------------------------------------------------------