1<?php
2// declare(strict_types=1);
3
4use ILIAS\HTTP\Cookies\CookieFactory;
5use ILIAS\HTTP\Cookies\CookieFactoryImpl;
6use ILIAS\HTTP\GlobalHttpState;
7use ILIAS\WebAccessChecker\HttpServiceAware;
8use ILIAS\WebAccessChecker\PathType;
9
10require_once('./Services/WebAccessChecker/class.ilWACException.php');
11require_once('class.ilWACToken.php');
12require_once('./Services/WebAccessChecker/classes/class.ilWebAccessChecker.php');
13require_once './Services/WebAccessChecker/interfaces/PathType.php';
14require_once './Services/WebAccessChecker/classes/HttpServiceAware.php';
15
16/**
17 * Class ilWACSignedPath
18 *
19 * @author  Fabian Schmid <fs@studer-raimann.ch>
20 * @version 1.0.0
21 */
22class ilWACSignedPath
23{
24    use HttpServiceAware;
25    const WAC_TOKEN_ID = 'il_wac_token';
26    const WAC_TIMESTAMP_ID = 'il_wac_ts';
27    const WAC_TTL_ID = 'il_wac_ttl';
28    const TS_SUFFIX = 'ts';
29    const TTL_SUFFIX = 'ttl';
30    const MAX_LIFETIME = 600;
31    /**
32     * @var ilWACPath
33     */
34    protected $path_object = null;
35    /**
36     * @var ilWACToken
37     */
38    protected $token_instance = null;
39    /**
40     * @var int
41     */
42    protected $type = PathType::FILE;
43    /**
44     * @var int
45     */
46    protected static $token_max_lifetime_in_seconds = 20;
47    /**
48     * @var int
49     */
50    protected static $cookie_max_lifetime_in_seconds = 300;
51    /**
52     * @var bool
53     */
54    protected $checked = false;
55    /**
56     * @var \ILIAS\DI\HTTPServices $httpService
57     */
58    private $httpService;
59    /**
60     * @var CookieFactory $cookieFactory
61     */
62    private $cookieFactory;
63
64
65    /**
66     * ilWACSignedPath constructor.
67     *
68     * @param \ilWACPath $ilWACPath
69     * @param GlobalHttpState $httpState
70     * @param CookieFactory $cookieFactory
71     */
72    public function __construct(ilWACPath $ilWACPath, GlobalHttpState $httpState, CookieFactory $cookieFactory)
73    {
74        $this->setPathObject($ilWACPath);
75        $this->httpService = $httpState;
76        $this->cookieFactory = $cookieFactory;
77    }
78
79
80    /**
81     * @return string
82     * @throws ilWACException
83     */
84    public function getSignedPath()
85    {
86        if ($this->getType() !== PathType::FILE) {
87            throw new ilWACException(ilWACException::WRONG_PATH_TYPE);
88        }
89        if (!$this->getPathObject()->getOriginalRequest()) {
90            return '';
91        }
92        if (!$this->getPathObject()->fileExists()) {
93            //			return $this->getPathObject()->getOriginalRequest();
94        }
95
96        if (strpos($this->getPathObject()->getPath(), '?')) {
97            $path = $this->getPathObject()->getPath() . '&' . self::WAC_TOKEN_ID . '='
98                    . $this->getTokenInstance()->getToken();
99        } else {
100            $path = $this->getPathObject()->getPath() . '?' . self::WAC_TOKEN_ID . '='
101                    . $this->getTokenInstance()->getToken();
102        }
103
104        $path = $path . '&' . self::WAC_TTL_ID . '=' . $this->getTokenInstance()->getTTL();
105        $path = $path . '&' . self::WAC_TIMESTAMP_ID . '='
106                . $this->getTokenInstance()->getTimestamp();
107
108        return $path;
109    }
110
111
112    /**
113     * @return bool
114     */
115    public function isFolderSigned()
116    {
117        $jar = $this->httpService->cookieJar();
118        $cookies = $jar->getAll();
119
120        $this->setType(PathType::FOLDER);
121        $plain_token = $this->buildTokenInstance();
122        $name = $plain_token->getHashedId();
123
124        // Token
125        $default_token = '';
126        $token_cookie_value = $this->httpService->request()->getCookieParams()[$name] ?? $default_token;
127        // Timestamp
128        $default_timestamp = 0;
129        $timestamp_cookie_value = $this->httpService->request()->getCookieParams()[$name . self::TS_SUFFIX] ?? $default_timestamp;
130        $timestamp_cookie_value = intval($timestamp_cookie_value);
131        // TTL
132        $default_ttl = 0;
133        $ttl_cookie_value = $this->httpService->request()->getCookieParams()[$name . self::TTL_SUFFIX] ?? $default_ttl;
134        $ttl_cookie_value = intval($ttl_cookie_value);
135
136        $this->getPathObject()->setToken($token_cookie_value);
137        $this->getPathObject()->setTimestamp($timestamp_cookie_value);
138        $this->getPathObject()->setTTL($ttl_cookie_value);
139        $this->buildAndSetTokenInstance();
140
141        return $this->getPathObject()->hasToken();
142    }
143
144
145    /**
146     * @return bool
147     * @throws ilWACException
148     */
149    public function isFolderTokenValid()
150    {
151        if (!$this->isFolderSigned()) {
152            return false;
153        }
154
155        return $this->checkToken();
156    }
157
158
159    /**
160     * @return void
161     */
162    protected function saveFolderToken()
163    {
164        $ttl = $this->getPathObject()->getTTL();
165        $cookie_lifetime = $ttl !== 0 ? $ttl : self::getCookieMaxLifetimeInSeconds();
166        $id = $this->getTokenInstance()->getHashedId();
167        $expire = time() + $cookie_lifetime + 3600;
168        $secure = true;
169        $domain = null;
170        $http_only = true;
171        $path = '/';
172
173        $tokenCookie = $this->cookieFactory->create($id, $this->getTokenInstance()->getToken())
174                                           ->withExpires($expire)
175                                           ->withPath($path)
176                                           ->withSecure($secure)
177                                           ->withDomain($domain)
178                                           ->withHttpOnly($http_only);
179
180        $timestampCookie = $this->cookieFactory->create($id . self::TS_SUFFIX, time())
181                                               ->withExpires($expire)
182                                               ->withPath($path)
183                                               ->withDomain($domain)
184                                               ->withSecure($secure)
185                                               ->withHttpOnly($http_only);
186
187        $ttlCookie = $this->cookieFactory->create($id . self::TTL_SUFFIX, $cookie_lifetime)
188                                         ->withExpires($expire)
189                                         ->withPath($path)
190                                         ->withDomain($domain)
191                                         ->withSecure($secure)
192                                         ->withHttpOnly($http_only);
193
194        $jar = $this->httpService->cookieJar()->with($tokenCookie)
195                                 ->with($timestampCookie)
196                                 ->with($ttlCookie);
197
198        // FIX: currently the cookies are never stored, we must use setcookie
199        foreach ($jar->getAll() as $cookie) {
200            setcookie($cookie->getName(), $cookie->getValue(), $cookie->getExpires(), $cookie->getPath(),
201                $cookie->getDomain(), $cookie->getSecure(), $cookie->getHttpOnly());
202        }
203    }
204
205
206    /**
207     * @return bool
208     */
209    public function revalidatingFolderToken()
210    {
211        if ($this->getType() !== PathType::FOLDER) {
212            return false;
213        }
214        $this->buildAndSetTokenInstance(time(), $this->getPathObject()->getTTL());
215        $this->getPathObject()->setTTL($this->getTokenInstance()->getTTL());
216        $this->getPathObject()->setTimestamp($this->getTokenInstance()->getTimestamp());
217        $this->getPathObject()->setToken($this->getTokenInstance()->getToken());
218
219        $this->saveFolderToken();
220
221        return true;
222    }
223
224
225    /**
226     * @return bool
227     */
228    public function isSignedPath()
229    {
230        return ($this->getPathObject()->hasToken() && $this->getPathObject()->hasTimestamp()
231                && $this->getPathObject()->hasTTL());
232    }
233
234
235    /**
236     * @return bool
237     * @throws ilWACException
238     */
239    public function isSignedPathValid()
240    {
241        $this->buildAndSetTokenInstance($this->getPathObject()->getTimestamp(), $this->getPathObject()->getTTL());
242
243        return $this->checkToken();
244    }
245
246
247    /**
248     * @param string $path_to_file
249     *
250     * @return string
251     *
252     * @throws ilWACException
253     */
254    public static function signFile($path_to_file)
255    {
256        if (!$path_to_file) {
257            return '';
258        }
259        $ilWACPath = new ilWACPath($path_to_file);
260        if (!$ilWACPath->getClient()) {
261            return $path_to_file;
262        }
263        $obj = new self($ilWACPath, self::http(), new CookieFactoryImpl());
264        $obj->setType(PathType::FILE);
265        $obj->buildAndSetTokenInstance(time(), self::getTokenMaxLifetimeInSeconds());
266
267        return $obj->getSignedPath();
268    }
269
270
271    /**
272     * @param string $start_file_path
273     * @return void
274     */
275    public static function signFolderOfStartFile($start_file_path)
276    {
277        $obj = new self(new ilWACPath($start_file_path), self::http(), new CookieFactoryImpl());
278        $obj->setType(PathType::FOLDER);
279        $obj->buildAndSetTokenInstance(time(), self::getCookieMaxLifetimeInSeconds());
280        $obj->saveFolderToken();
281    }
282
283
284    /**
285     * @return ilWACToken
286     */
287    public function getTokenInstance()
288    {
289        return $this->token_instance;
290    }
291
292
293    /**
294     * @param ilWACToken $token_instance
295     * @return void
296     */
297    public function setTokenInstance(ilWACToken $token_instance)
298    {
299        $this->token_instance = $token_instance;
300    }
301
302
303    /**
304     * @return int
305     */
306    public function getType()
307    {
308        return (int) $this->type;
309    }
310
311
312    /**
313     * @param int $type
314     * @return void
315     */
316    public function setType($type)
317    {
318        assert(is_int($type));
319        $this->type = $type;
320    }
321
322
323    /**
324     * @return ilWACPath
325     */
326    public function getPathObject()
327    {
328        return $this->path_object;
329    }
330
331
332    /**
333     * @param ilWACPath $path_object
334     * @return void
335     */
336    public function setPathObject(ilWACPath $path_object)
337    {
338        $this->path_object = $path_object;
339    }
340
341
342    /**
343     * @return bool
344     * @throws \ilWACException
345     */
346    protected function checkToken()
347    {
348        $request_token_string = $this->getPathObject()->getToken();
349        $request_ttl = $this->getPathObject()->getTTL();
350        $request_timestamp = $this->getPathObject()->getTimestamp();
351        $current_timestamp = time();
352
353        $timestamp_valid = ($current_timestamp < ($request_timestamp + $request_ttl));
354
355        if (!$timestamp_valid) {
356            $this->setChecked(true);
357
358            return false;
359        }
360
361        $simulated_token = $this->buildTokenInstance($request_timestamp, $request_ttl);
362        $simulated_token_string = $simulated_token->getToken();
363        $token_valid = ($simulated_token_string == $request_token_string);
364
365        if (!$token_valid) {
366            $this->setChecked(true);
367
368            return false;
369        }
370
371        return true;
372    }
373
374
375    /**
376     * @param int $timestamp
377     * @param int $ttl
378     *
379     * @return ilWACToken
380     * @throws ilWACException
381     */
382    protected function buildTokenInstance($timestamp = 0, $ttl = 0)
383    {
384        assert(is_int($timestamp));
385        assert(is_int($ttl));
386        if (!$this->getType()) {
387            throw new ilWACException(ilWACException::CODE_NO_TYPE);
388        }
389
390        switch ($this->getType()) {
391            case PathType::FOLDER:
392                $path = $this->getPathObject()->getSecurePath();
393                break;
394            default:
395                $path = $this->getPathObject()->getPathWithoutQuery();
396                break;
397        }
398
399        $client = $this->getPathObject()->getClient();
400        $timestamp = $timestamp !== 0 ? $timestamp : $this->getPathObject()->getTimestamp();
401        $ttl = $ttl !== 0 ? $ttl : $this->getPathObject()->getTTL();
402
403        return new ilWACToken($path, $client, $timestamp, $ttl);
404    }
405
406
407    /**
408     * @param int $timestamp
409     * @param int $ttl
410     * @return void
411     *
412     * @throws \ilWACException
413     */
414    public function buildAndSetTokenInstance($timestamp = 0, $ttl = 0)
415    {
416        assert(is_int($timestamp));
417        assert(is_int($ttl));
418
419        $this->setTokenInstance($this->buildTokenInstance($timestamp, $ttl));
420    }
421
422
423    /**
424     * @return int
425     */
426    public static function getTokenMaxLifetimeInSeconds()
427    {
428        return self::$token_max_lifetime_in_seconds;
429    }
430
431
432    /**
433     * @param int $token_max_lifetime_in_seconds
434     * @return void
435     *
436     * @throws \ilWACException
437     */
438    public static function setTokenMaxLifetimeInSeconds($token_max_lifetime_in_seconds)
439    {
440        assert(is_int($token_max_lifetime_in_seconds));
441        if ($token_max_lifetime_in_seconds > self::MAX_LIFETIME) {
442            throw new ilWACException(ilWACException::MAX_LIFETIME);
443        }
444        self::$token_max_lifetime_in_seconds = $token_max_lifetime_in_seconds;
445    }
446
447
448    /**
449     * @return int
450     */
451    public static function getCookieMaxLifetimeInSeconds()
452    {
453        return self::$cookie_max_lifetime_in_seconds;
454    }
455
456
457    /**
458     * @param int $cookie_max_lifetime_in_seconds
459     *
460     * @return void
461     *
462     * @throws \ilWACException
463     */
464    public static function setCookieMaxLifetimeInSeconds($cookie_max_lifetime_in_seconds)
465    {
466        assert(is_int($cookie_max_lifetime_in_seconds));
467        if ($cookie_max_lifetime_in_seconds > self::MAX_LIFETIME) {
468            throw new ilWACException(ilWACException::MAX_LIFETIME);
469        }
470        self::$cookie_max_lifetime_in_seconds = $cookie_max_lifetime_in_seconds;
471    }
472
473
474    /**
475     * @return int
476     */
477    protected function getRelevantLifeTime()
478    {
479        $request_ttl = $this->getPathObject()->getTTL();
480        if ($request_ttl > 0) {
481            return $request_ttl;
482        }
483        switch ($this->getType()) {
484            case PathType::FOLDER:
485                $life_time = self::getCookieMaxLifetimeInSeconds();
486                break;
487            case PathType::FILE:
488                $life_time = self::getTokenMaxLifetimeInSeconds();
489                break;
490            default:
491                $life_time = 0;
492                break;
493        }
494
495        return $life_time;
496    }
497
498
499    /**
500     * @return bool
501     */
502    public function isChecked()
503    {
504        return (bool) $this->checked;
505    }
506
507
508    /**
509     * @param bool $checked
510     * @return void
511     */
512    public function setChecked($checked)
513    {
514        assert(is_bool($checked));
515        $this->checked = $checked;
516    }
517}
518