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