├── 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 |
--------------------------------------------------------------------------------