ILIAS  trunk Revision v13.0_alpha-62-g3b72d83f2e5
Crawler.php
Go to the documentation of this file.
1<?php
2
19declare(strict_types=1);
20
21namespace ILIAS\WOPI\Discovery;
22
24
29{
33 private const XPATH = '/wopi-discovery/net-zone/app';
38 public const ALLOWED_SCHEMES = ['https', 'http'];
39 private const TIMEOUT_SECONDS = 10;
40 private const MAX_REDIRECTS = 3;
41 private const MAX_SIZE_BYTES = 2 * 1024 * 1024;
42 private array $crawl_actions = [];
43 private ?string $content = null;
44 private ?\SimpleXMLElement $discovery = null;
48 private ?array $xml_app_elements = null;
49
50 public function __construct()
51 {
52 $this->crawl_actions = [
53 ActionTarget::VIEW->value,
54 ActionTarget::EMBED_VIEW->value,
55 ActionTarget::EDIT->value,
56 ActionTarget::EMBED_EDIT->value,
57 ActionTarget::EDIT_NEW->value,
58 ActionTarget::CONVERT->value,
59 ];
60 }
61
62 public static function isWebUrl(URI $url): bool
63 {
64 return in_array(strtolower($url->getSchema()), self::ALLOWED_SCHEMES, true);
65 }
66
67 public function validate(URI $discovery_url): bool
68 {
69 if (!self::isWebUrl($discovery_url)) {
70 return false;
71 }
72 try {
73 return $this->load($this->fetch($discovery_url));
74 } catch (\Throwable) {
75 return false;
76 }
77 }
78
79 private function fetch(URI $discovery_url): ?string
80 {
81 $context = stream_context_create([
82 'http' => [
83 'timeout' => self::TIMEOUT_SECONDS,
84 'max_redirects' => self::MAX_REDIRECTS,
85 ],
86 ]);
87 // one byte more than allowed tells a discovery which is too large
88 $content = @file_get_contents((string) $discovery_url, false, $context, 0, self::MAX_SIZE_BYTES + 1);
89 if ($content === false || $content === '' || strlen($content) > self::MAX_SIZE_BYTES) {
90 return null;
91 }
92
93 return $content;
94 }
95
96 private function load(?string $content): bool
97 {
98 try {
99 $this->content = $content;
100 if ($this->content === null) {
101 return false;
102 }
103
104 // invalid XML is reported by returning false, not by warnings
105 $use_internal_errors = libxml_use_internal_errors(true);
106 $this->discovery = simplexml_load_string($this->content) ?: null;
107 libxml_clear_errors();
108 libxml_use_internal_errors($use_internal_errors);
109 if ($this->discovery === null) {
110 return false;
111 }
112 $this->xml_app_elements = $this->discovery->xpath(self::XPATH);
113
114 return is_array($this->xml_app_elements);
115 } catch (\Throwable) {
116 return false;
117 }
118 }
119
120 public function crawl(URI $discovery_url): ?Apps
121 {
122 if (!$this->validate($discovery_url)) {
123 return null;
124 }
125
126 return $this->parse();
127 }
128
132 public function crawlContent(string $content): ?Apps
133 {
134 if (!$this->load($content)) {
135 return null;
136 }
137
138 return $this->parse();
139 }
140
144 public function getProofKeys(): ?ProofKeys
145 {
146 $element = $this->discovery?->xpath('/wopi-discovery/proof-key')[0] ?? null;
147 if (!$element instanceof \SimpleXMLElement) {
148 return null;
149 }
150 $modulus = (string) ($element['modulus'] ?? '');
151 $exponent = (string) ($element['exponent'] ?? '');
152 if ($modulus === '' || $exponent === '') {
153 return null;
154 }
155 $old_modulus = (string) ($element['oldmodulus'] ?? '');
156 $old_exponent = (string) ($element['oldexponent'] ?? '');
157
158 return new ProofKeys(
159 new ProofKey($modulus, $exponent),
160 $old_modulus === '' || $old_exponent === '' ? null : new ProofKey($old_modulus, $old_exponent)
161 );
162 }
163
164 private function parse(): Apps
165 {
166 // read wopi-discovery XML from $discovery_url and parse Apps with it's Actions
167 $apps = [];
168 foreach ($this->xml_app_elements as $app) {
169 $actions = [];
170 foreach ($app->action as $action) {
171 $action_name = $action['name'] ?? null;
172 $action_ext = $action['ext'] ?? null;
173 $action_urlsrc = $action['urlsrc'] ?? null;
174 // the extension of the resulting file: targetext for convert, newext for editnew
175 $target_text = isset($action['targetext'])
176 ? (string) $action['targetext']
177 : (isset($action['newext']) ? (string) $action['newext'] : null);
178 if (!$action_name instanceof \SimpleXMLElement) {
179 continue;
180 }
181 if (!$action_ext instanceof \SimpleXMLElement) {
182 continue;
183 }
184 if (!$action_urlsrc instanceof \SimpleXMLElement) {
185 continue;
186 }
187
188 if (!in_array((string) $action_name, $this->crawl_actions, true)) {
189 continue;
190 }
191
192 $uri_string = rtrim((string) $action_urlsrc, '?');
193 // remove all after ?
194 $uri = explode('?', $uri_string);
195 $uri_string = $uri[0];
196 try {
197 $launcher_url = new URI($uri_string);
198 } catch (\Throwable) {
199 continue;
200 }
201 if (!self::isWebUrl($launcher_url)) {
202 continue;
203 }
204 $actions[] = new Action(
205 0,
206 (string) $action_name,
207 (string) $action_ext,
208 $launcher_url,
209 $uri[1] ?? null,
210 $target_text
211 );
212 }
213 if ($actions === []) {
214 continue;
215 }
216
217 $app_name = $app['name'] ?? null;
218 if ($app_name === null) {
219 continue;
220 }
221 $apps[] = new App(
222 0,
223 (string) $app_name,
224 $actions,
225 $this->favIconUrl($app)
226 );
227 }
228 return new Apps($apps);
229 }
230
231 private function favIconUrl(\SimpleXMLElement $app): ?URI
232 {
233 $value = $app['favIconUrl'] ?? null;
234 if ($value === null) {
235 return null;
236 }
237 try {
238 $url = new URI((string) $value);
239 } catch (\Throwable) {
240 return null;
241 }
242
243 return self::isWebUrl($url) ? $url : null;
244 }
245}
The scope of this class is split ilias-conform URI's into components.
Definition: URI.php:35
favIconUrl(\SimpleXMLElement $app)
Definition: Crawler.php:231
crawl(URI $discovery_url)
Definition: Crawler.php:120
static isWebUrl(URI $url)
Definition: Crawler.php:62
load(?string $content)
Definition: Crawler.php:96
crawlContent(string $content)
Parses discovery XML that has already been fetched.
Definition: Crawler.php:132
getProofKeys()
The proof keys of the discovery read last, null if it publishes none.
Definition: Crawler.php:144
SimpleXMLElement $discovery
Definition: Crawler.php:44
const ALLOWED_SCHEMES
the discovery and the URLs it publishes must be web URLs, anything else (file, php,...
Definition: Crawler.php:38
validate(URI $discovery_url)
Definition: Crawler.php:67
fetch(URI $discovery_url)
Definition: Crawler.php:79
A public RSA key of a WOPI client, as published in its discovery with base64 encoded modulus and expo...
Definition: ProofKey.php:30
The current and the previous proof key of a WOPI client.
Definition: ProofKeys.php:30