ILIAS  release_9 Revision v9.13-25-g2c18ec4c24f
RequestHandler.php
Go to the documentation of this file.
1 <?php
2 
19 declare(strict_types=1);
20 
22 
28 
32 final class RequestHandler
33 {
34  public const WOPI_BASE_URL = '/wopi/index.php/';
35  public const NAMESPACE_FILES = 'files';
36 
37  // WOPI Header
38  private const HEADER_X_REQUEST_ID = 'X-Request-ID';
39  private const HEADER_AUTHORIZATION = 'Authorization';
40  private const HEADER_AUTHORIZATION_BEARER = 'Bearer';
41  public const HEADER_X_WOPI_OVERRIDE = 'X-WOPI-Override';
42  public const HEADER_X_WOPI_LOCK = 'X-WOPI-Lock';
43  public const HEADER_X_WOPI_FILE_CONVERSION = 'X-WOPI-FileConversion';
44 
48  private Services $http;
52  private \ILIAS\ResourceStorage\Services $irss;
57  private ?int $token_user_id = null;
58  private ?string $token_resource_id = null;
60  private int $saving_interval = 0;
61  private bool $editable = false;
62 
63  public function __construct()
64  {
65  global $DIC;
66  $this->http = $DIC->http();
67  $this->data_signer = $DIC['file_delivery.data_signer'];
68  $this->irss = $DIC->resourceStorage();
69  }
70 
71  protected function checkAuth(): void
72  {
73  $auth = $this->http->request()->getHeader(self::HEADER_AUTHORIZATION)[0] ?? '';
74  // spit and check bearer token
75  $bearer = explode(' ', $auth);
76  if ($auth !== '' && ($bearer[0] ?? '') !== self::HEADER_AUTHORIZATION_BEARER) {
77  throw new \InvalidArgumentException();
78  }
79  $bearer_token = $bearer[1] ?? '';
80  if ($auth === '' && $bearer_token === '') {
81  // we try to get the token from GET
82  $bearer_token = $this->http->request()->getQueryParams()['access_token'] ?? '';
83  }
84  if ($bearer_token === '') {
85  throw new \InvalidArgumentException('No access token provided');
86  }
87  if (($token_data = $this->data_signer->verify($bearer_token, 'wopi')) === null) {
88  throw new \InvalidArgumentException('Token verification failed');
89  }
90 
91  $this->token_user_id = (int) ($token_data['user_id'] ?? 0);
92  $this->token_resource_id = (string) ($token_data['resource_id'] ?? '');
93  $this->editable = (bool) ($token_data['editable'] ?? '');
94 
95  // Init Stakeholder
96  $stakeholder = $token_data['stakeholder'] ?? WOPIUnknownStakeholder::class;
97  try {
98  $this->stakeholder = new WOPIStakeholderWrapper();
99  $this->stakeholder->init(new $stakeholder(), $this->token_user_id);
100  } catch (\Throwable) {
101  $this->stakeholder = new WOPIUnknownStakeholder();
102  }
103  }
104 
108  public function handleRequest(): void
109  {
110  try {
111  $this->checkAuth();
112 
113  $uri = $this->http->request()->getUri()->getPath();
114  $request = substr($uri, strpos($uri, self::WOPI_BASE_URL) + strlen(self::WOPI_BASE_URL));
115  $request = explode('/', $request);
116  $method = $this->http->request()->getMethod();
117 
118  $resource_id = $request[1];
119  $action = $request[2] ?? '';
120 
121  // check resource_id
122  if ($this->token_resource_id !== $resource_id) {
123  $this->http->close();
124  }
125 
126  $resource_id = $this->irss->manage()->find($resource_id);
127  if (!$resource_id instanceof ResourceIdentification) {
128  $this->http->close();
129  }
130  $resource = $this->irss->manage()->getResource($resource_id);
131  if ($this->editable) {
132  $current_revision = $resource->getCurrentRevisionIncludingDraft();
133  } else {
134  $current_revision = $resource->getCurrentRevision();
135  }
136 
137  $method_override = $this->http->request()->getHeader(self::HEADER_X_WOPI_OVERRIDE)[0] ?? null;
138  $is_file_convertion = (bool) ($this->http->request()->getHeader(
139  self::HEADER_X_WOPI_FILE_CONVERSION
140  )[0] ?? false);
141 
142  // GET
143  switch ($method_override ?? $method) {
144  case 'GET':
145  switch ($action) {
146  case '':
147  // CheckFileInfo
149  $current_revision,
150  $this->token_user_id,
151  $this->editable
152  );
153  $this->http->saveResponse(
154  $this->http->response()->withBody(
155  Streams::ofString(json_encode($response, JSON_THROW_ON_ERROR))
156  )
157  );
158 
159  break;
160  case 'contents':
161  // GetFile
162  $stream = $this->irss->consume()->stream($resource_id)->setRevisionNumber(
163  $current_revision->getVersionNumber()
164  )->getStream();
165  $this->http->saveResponse(
166  $this->http->response()->withBody($stream)
167  );
168 
169  break;
170  }
171  break;
172  case 'PUT_RELATIVE':
173  if (!$is_file_convertion) {
174  throw new \InvalidArgumentException();
175  }
176  // no break
177  case 'PUT':
178  switch ($action) {
179  case 'contents':
180  // PutFile
181  $body_stream = $this->http->request()->getBody();
182  $body = $body_stream->getContents();
183  $file_stream = Streams::ofString($body);
184 
185  $draft = true;
186 
187  if ($this->saving_interval > 0) {
188  $latest_revision = $resource->getCurrentRevision();
189  $creation_time = $latest_revision->getInformation()->getCreationDate()->getTimestamp();
190  $current_time = time();
191  $time_diff = $current_time - $creation_time;
192  if ($time_diff > $this->saving_interval) {
193  $this->irss->manage()->publish($resource_id);
194  }
195  }
196 
197  $new_revision = $this->irss->manage()->appendNewRevisionFromStream(
198  $resource_id,
199  $file_stream,
200  $this->stakeholder,
201  $current_revision->getTitle(),
202  $draft
203  );
204 
205  // CheckFileInfo
207  $new_revision,
208  $this->token_user_id
209  );
210  $this->http->saveResponse(
211  $this->http->response()->withBody(
212  Streams::ofString(json_encode($response, JSON_THROW_ON_ERROR))
213  )
214  );
215 
216  break;
217  case '': // if we want to create new files in the future, this will be a separate case
218  break;
219  }
220  break;
221  case 'POST':
222  switch ($action) {
223  case 'contents':
224  $this->http->saveResponse(
225  $this->http->response()->withBody(
227  )
228  );
229 
230  break;
231  case '':
232  // Lock
233  $lock = $this->http->request()->getHeader(self::HEADER_X_WOPI_LOCK)[0] ?? null;
234  $this->http->saveResponse(
235  $this->http->response()->withBody(
237  )
238  );
239  }
240  break;
241  }
242  } catch (\Throwable $t) {
243  $message = $t->getMessage();
244  // append simple stacktrace
245  $trace = array_map(
246  static fn(array $trace): string => $trace['file'] . ':' . $trace['line'],
247  $t->getTrace()
248  );
249 
250  $message .= "\n" . implode("\n", $trace);
251 
252  $this->http->saveResponse(
253  $this->http->response()
254  ->withBody(Streams::ofString($message))
255  ->withStatus(500)
256  ->withHeader('X-WOPI-ServerError', $t->getMessage())
257  );
258  }
259  $this->http->sendResponse();
260  $this->http->close();
261  }
262 }
ILIAS ResourceStorage Services $irss
$response
Definition: xapitoken.php:93
global $DIC
Definition: feed.php:28
static http()
Fetches the global http state from ILIAS.
static ofString(string $string)
Creates a new stream with an initial value.
Definition: Streams.php:41
$message
Definition: xapiexit.php:32