File modules/util/Http.class.php

Last commit: Wed Feb 2 01:12:42 2022 +0100	dankert	Better support for API requests.
1 <?php 2 // OpenRat Content Management System 3 // Copyright (C) 2002-2012 Jan Dankert, cms@jandankert.de 4 // 5 // This program is free software; you can redistribute it and/or 6 // modify it under the terms of the GNU General Public License 7 // as published by the Free Software Foundation; either version 2 8 // of the License, or (at your option) any later version. 9 // 10 // This program is distributed in the hope that it will be useful, 11 // but WITHOUT ANY WARRANTY; without even the implied warranty of 12 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 // GNU General Public License for more details. 14 // 15 // You should have received a copy of the GNU General Public License 16 // along with this program; if not, write to the Free Software 17 // Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. 18 19 20 namespace util; 21 use cms\base\DB; 22 use logger\Logger; 23 use withPraefixQuestionMark; 24 25 /** 26 * Kapselung einer HTTP-Anfrage.<br> 27 * Unter Beruecksichtigung von RFC 1945.<br> 28 * 29 * @author Jan Dankert 30 * @package openrat.services 31 */ 32 class Http 33 { 34 public $header = array(); 35 36 public $url = array(); 37 public $responseHeader = array(); 38 public $requestParameter = array(); 39 public $urlParameter = array(); 40 41 /** 42 * HTTP-Request-Typ.<br> 43 * Muss entweder "GET" oder "POST" sein.<br> 44 * Default: "GET". 45 * 46 * @var String Request-Typ 47 */ 48 public $method = 'GET'; 49 public $error = ''; 50 public $status = ''; 51 public $body = ''; 52 53 public $httpCmd = ''; 54 55 56 /** 57 * Erzeugt eine HTTP-Anfrage. 58 * 59 * @param String URL 60 * @return Http 61 */ 62 public function __construct($url = '') 63 { 64 $this->setURL($url); 65 $this->header['User-Agent'] = 'Mozilla/5.0 (OpenRat CMS)'; 66 $this->header['Connection'] = 'close'; 67 } 68 69 70 /** 71 * Setzt die URL. 72 * 73 * @param String URL 74 */ 75 function setURL($url) 76 { 77 $this->url = parse_url($url); 78 79 if (empty($this->url['host']) && !empty($this->url['path'])) { 80 $this->url['host'] = basename($this->url['path']); 81 $this->url['path'] = '/'; 82 } 83 84 if (empty($this->url['path'])) 85 $this->url['path'] = '/'; 86 87 if (!isset($this->url['port'])) 88 if (!isset($this->url['scheme'])) { 89 $this->url['scheme'] = 'http'; // Standard-Port. 90 $this->url['port'] = 80; // Standard-Port. 91 } elseif ($this->url['scheme'] == 'https') 92 $this->url['port'] = 443; // SSL-Port. 93 else 94 $this->url['port'] = 80; // Standard-Port. 95 96 if (!empty($this->url['query'])) 97 parse_str($this->url['query'], $this->urlParameter); 98 99 } 100 101 102 /** 103 * Setzt Authentisierungsinformationen in den HTTP-Request.<br> 104 * 105 * @param String Benutzername 106 * @param String Kennwort 107 */ 108 public function setBasicAuthentication($user, $password) 109 { 110 $this->header['Authorization'] = 'Basic ' . base64_encode($user . ':' . $password); 111 } 112 113 114 /** 115 * Erzeugt eine HTTP-Parameterstring mit allen Parametern. 116 * 117 * @param withPraefixQuestionMark Praefix mit Fragezeichen (fuer GET-Anfragen) 118 * @return String URL-Parameter 119 */ 120 public function getParameterString($withPraefixQuestionMark = false) 121 { 122 $parameterString = ''; 123 $parameter = $this->urlParameter + $this->requestParameter; 124 125 if (!empty($parameter)) { 126 foreach ($this->requestParameter as $paramName => $paramValue) { 127 if (strlen($parameterString) > 0) 128 $parameterString .= '&'; 129 elseif ($withPraefixQuestionMark) 130 $parameterString .= '?'; 131 132 $parameterString .= urlencode($paramName) . '=' . urlencode($paramValue); 133 } 134 } 135 136 return $parameterString; 137 } 138 139 140 /** 141 * Liefert die URL des Requests. 142 * 143 * @return String URL 144 */ 145 public function getUrl() 146 { 147 $location = $this->url['scheme']; 148 $location .= '://'; 149 $location .= $this->url['host']; 150 if ($this->url['scheme'] == 'http' && $this->url['port'] != 80 || 151 $this->url['scheme'] == 'https' && $this->url['port'] != 443) 152 $location .= ':' . $this->url['port']; 153 $location .= $this->url['path']; 154 155 $location .= $this->getParameterString(true); 156 157 if (isset($this->url['fragment'])) 158 $location .= '#' . $this->url['fragment']; 159 160 return $location; 161 } 162 163 164 /** 165 * Sendet eine Redirect-Anweisung mit der aktuellen URL an den Browser. 166 */ 167 public function sendRedirect() 168 { 169 $location = $this->getUrl(); 170 171 header('Location: ' . $location); 172 exit; 173 } 174 175 176 /** 177 * Führt einen HTTP-Request durch. 178 * 179 * @return boolean Erfolg der Anfrage. 180 */ 181 public function request() 182 { 183 $this->error = ''; 184 $this->status = ''; 185 186 $errno = 0; 187 $errstr = ''; 188 189 if (empty($this->url['host'])) { 190 $this->error = "No hostname specified"; 191 return false; 192 } 193 194 foreach ($this->header as $header_key => $header_value) { 195 if (is_numeric($header_key)) { 196 $dp = strpos($header_value, ':'); 197 if ($dp !== FALSE) 198 $this->header[substr($header_value, 0, $dp)] = substr($header_value, $dp + 1); 199 unset($this->header[$header_key]); 200 } 201 } 202 203 $parameterString = $this->getParameterString(); 204 205 if ($this->method == 'POST') { 206 $this->header['Content-Type'] = 'application/x-www-form-urlencoded'; 207 $this->header['Content-Length'] = strlen($parameterString); 208 } 209 210 // Accept-Header setzen, falls noch nicht vorhanden. 211 if (!array_key_exists('Accept', $this->header)) 212 $this->header['Accept'] = '*/*'; 213 214 $this->responseHeader = array(); 215 216 // RFC 1945 (Section 9.3) says: 217 // A user agent should never automatically redirect a request 218 // more than 5 times, since such redirections usually indicate an infinite loop. 219 for ($r = 1; $r <= 5; $r++) { 220 $this->header['Host'] = $this->url['host']; 221 222 // Die Funktion fsockopen() erwartet eine Protokollangabe (bei TCP optional, bei SSL notwendig). 223 if ($this->url['scheme'] == 'https' || $this->url['port'] == '443') 224 $prx_proto = 'ssl://'; // SSL 225 else 226 $prx_proto = 'tcp://'; // Default 227 228 $fp = @fsockopen($prx_proto . $this->url['host'], $this->url['port'], $errno, $errstr, 30); 229 230 if (!$fp || !is_resource($fp)) { 231 // Keine Verbindung zum Host moeglich. 232 $this->error = "Connection refused: '" . $prx_proto . $this->url['host'] . ':' . $this->url['port'] . " - $errstr ($errno)"; 233 return false; 234 } else { 235 236 $lb = "\r\n"; 237 $http_get = $this->url['path']; 238 239 $request_header = array($this->method . ' ' . $http_get . ' HTTP/1.0'); 240 241 foreach ($this->header as $header_key => $header_value) 242 $request_header[] = $header_key . ': ' . $header_value; 243 244 $http_request = implode($lb, $request_header) . $lb . $lb; 245 246 if ($this->method == 'GET') 247 if (!empty($parameterString)) 248 $http_get .= '?' . $parameterString; 249 250 if ($this->method == 'POST') 251 $http_request .= $parameterString; 252 253 if (!is_resource($fp)) { 254 $this->error = 'Connection lost after connect: ' . $prx_proto . $this->url['host'] . ':' . $this->url['port']; 255 return false; 256 } 257 fputs($fp, $http_request); // Die HTTP-Anfrage zum Server senden. 258 259 // Jetzt erfolgt das Auslesen der HTTP-Antwort. 260 $isHeader = true; 261 262 // RFC 1945 (Section 6.1) schreibt als Statuszeile folgendes Format vor 263 // "HTTP/" 1*DIGIT "." 1*DIGIT SP 3DIGIT SP 264 if (!is_resource($fp)) { 265 $this->error = 'Connection lost during transfer: ' . $this->url['host'] . ':' . $this->url['port']; 266 return false; 267 } elseif (!feof($fp)) { 268 $line = fgets($fp, 1028); 269 $this->status = substr($line, 9, 3); 270 } else { 271 $this->error = 'Unexpected EOF while reading HTTP-Response'; 272 return false; 273 } 274 275 $this->body = ''; 276 while (!feof($fp)) { 277 $line = fgets($fp, 1028); 278 if ($isHeader && trim($line) == '') // Leerzeile nach Header. 279 { 280 $isHeader = false; 281 } elseif ($isHeader) { 282 list($headerName, $headerValue) = explode(': ', $line) + array(1 => ''); 283 $this->responseHeader[$headerName] = trim($headerValue); 284 } else { 285 $this->body .= $line; 286 } 287 } 288 fclose($fp); // Verbindung brav schlie�en. 289 290 291 // RFC 1945 (Section 6.1.1) schreibt 292 // "[...] However, applications must understand the class of any status code, as 293 // indicated by the first digit" 294 // Daher interessiert uns nur die erste Stelle des 3-stelligen HTTP-Status. 295 296 // 301 Moved Permanently 297 // 302 Moved Temporarily 298 if ($this->status == '301' || 299 $this->status == '302') { 300 $location = @$this->responseHeader['Location']; 301 if (empty($location)) { 302 $this->error = '301/302 Response without Location-header'; 303 return false; 304 } 305 306 //Html::debug($this->url,"alte URL"); 307 //Html::debug($location,"NEUES REDIRECT AUF"); 308 $this->setURL($location); 309 continue; // Naechster Versuch mit umgeleiteter Adresse. 310 } 311 312 // RFC 1945 (Section 6.1.1) schreibt 313 // "2xx: Success - The action was successfully received, understood, and accepted." 314 elseif (substr($this->status, 0, 1) == '2') { 315 return true; 316 } elseif (substr($this->status, 0, 1) == '4') { 317 $this->error = 'Client Error: ' . $this->status; 318 return false; 319 } elseif (substr($this->status, 0, 1) == '5') { 320 $this->error = 'Server Error: ' . $this->status; 321 return false; 322 } else { 323 $this->error = 'Unexpected HTTP-Status: ' . $this->status . '; this is mostly a client error, sorry.'; 324 return false; 325 } 326 } 327 328 $this->error = 'Too much redirects, infinite loop assumed. Exiting. Last URL: ' . $http_get; 329 return false; 330 331 } 332 333 } 334 335 336 /** 337 * Aus dem HTTP-Header werden die vom Browser angeforderten Sprachen 338 * gelesen.<br> 339 * Es wird eine Liste von Sprachen erzeugt.<br> 340 * 341 * Beispiel: 342 * 'de_DE','de','en_GB','en' ... usw.<br> 343 * Wenn der Browser 'de_DE' anfordert, wird hier zusätzlich 344 * auch 'de' (als Fallback) ermittelt. 345 * 346 * @static 347 * @return array 348 */ 349 public static function getLanguages() 350 { 351 $languages = array(); 352 $http_languages = @$_SERVER['HTTP_ACCEPT_LANGUAGE']; 353 foreach (explode(',', $http_languages) as $l) { 354 list($part) = explode(';', $l); // Priorit�ten ignorieren. 355 $languages[] = trim($part); 356 357 // Aus "de_DE" das "de" extrahieren. 358 $languages[] = current(explode('_', str_replace('-', '_', trim($part)))); 359 } 360 361 return array_unique($languages); 362 } 363 364 365 /** 366 * Ermittelt die aktuelle HTTP-Adresse des Requests (inkl. Pfad, jedoch ohne Datei). 367 * 368 * @return String URL 369 */ 370 public static function getServer() 371 { 372 $https = getenv('HTTPS'); 373 374 if ($https) 375 $server = 'https://'; 376 else 377 $server = 'http://'; 378 379 $server .= getenv('SERVER_NAME') . dirname(getenv('REQUEST_URI')); 380 381 return $server; 382 } 383 384 385 386 public static function badRequest() { 387 self::sendStatus('400','Bad Request'); 388 } 389 390 public static function methodNotAllowed() { 391 self::sendStatus('405','Bad Request'); 392 } 393 394 public static function notAcceptable() { 395 self::sendStatus('406','Not Acceptable'); 396 } 397 398 /** 399 * Server-Fehlermeldung anzeigen.<br> 400 * 401 * Erzeugt einen "HTTP 501 Internal Server Error". Zusaetzlich 402 * wird ein 'rollback' auf der Datenbank ausgefaehrt. 403 * 404 * @param String $message Eigener Hinweistext 405 */ 406 public static function serverError($message = '', $reason = '') 407 { 408 /* 409 try { 410 411 if (class_exists('util\Session')) { 412 $db = DB::get(); 413 if (is_object($db)) 414 $db->rollback(); 415 } 416 417 if (class_exists('logger\Logger')) 418 Logger::warn($message . "\n" . $reason); 419 } 420 catch( \Exception $e ) { 421 //error_log( $e->__toString() ); 422 }*/ 423 424 self::sendStatus(500, 'Internal Server Error'); 425 } 426 427 428 /** 429 * Der Benutzer ist nicht autorisiert, eine Aktion auszufuehren. 430 * 431 * Diese Funktion erzeugt einen "HTTP 403 Not Authorized" und das 432 * Skript wird beendet. 433 * 434 * @param String $text Text 435 * @param String $message Eigener Hinweistext 436 */ 437 public static function forbidden() 438 { 439 Http::sendStatus(403, 'Forbidden'); 440 } 441 442 443 /** 444 * Nichts gefunden. 445 * 446 * Diese Funktion erzeugt einen "HTTP 404 Not found" und das 447 * Skript wird beendet. 448 * 449 */ 450 public static function notFound() 451 { 452 Http::sendStatus(404, 'Not found'); 453 } 454 455 456 /** 457 * Kein Inhalt. 458 * 459 * Die HTTP-Antwort stellt gegenüber dem Client klar, dass es keinen Inhalt gibt. 460 */ 461 public static function noContent() 462 { 463 self::sendStatus(204,'No Content'); 464 } 465 466 467 /** 468 * Schickt einen HTTP-Status zum Client und beendet das Skript. 469 * 470 * @param Integer $status HTTP-Status (ganzzahlig) (Default: 501) 471 * @param String $text HTTP-Meldung (Default: 'Internal Server Error') 472 */ 473 private static function sendStatus($status = 500, $text = 'Internal Server Error') 474 { 475 if (headers_sent()) { 476 echo "$status $text"; 477 exit; 478 } 479 480 header('HTTP/1.0 ' . intval($status) . ' ' . $text); 481 } 482 483 484 /** 485 * Liefert den Mime-Type, den der Browser (oder besser: HTTP-Client) wünscht. 486 * 487 * @return array Mime-Typen, welche vom User-Agent akzeptiert werden. 488 */ 489 public static function getAccept() 490 { 491 $httpAccept = getenv('HTTP_ACCEPT'); 492 return array_map( function($accept) { 493 return explode(';',$accept)[0]; 494 }, explode(',', $httpAccept) 495 ); 496 } 497 498 499 /** 500 * Liefert die IPv4-Adresse des Clients. Falls der Request durch einen Proxy kam, wird 501 * versucht, die echte IP-Adresse aus dem Anfrageheader zu ermitteln. 502 * 503 * @return Client-IPv4-Adresse 504 */ 505 public static function getClientIP() 506 { 507 $ip = ''; 508 509 if (isset($_SERVER["HTTP_X_FORWARDED_FOR"])) { 510 $ip = $_SERVER["HTTP_X_FORWARDED_FOR"]; 511 } elseif (isset($_SERVER["HTTP_CLIENT_IP"])) { 512 $ip = $_SERVER["HTTP_CLIENT_IP"]; 513 } elseif (isset($_SERVER["REMOTE_ADDR"])) { 514 $ip = $_SERVER["REMOTE_ADDR"]; 515 } 516 517 return $ip; 518 } 519 520 521 /** 522 * Ermittelt den TCP/IP-Port des Clients. 523 * Achtung, bei Proxy-Zugriffen kann dies der Port des Proxys sein. 524 * 525 * @return string TCP/IP-Port 526 */ 527 public static function getClientPort() 528 { 529 $ip = ''; 530 531 if (isset($_SERVER["REMOTE_PORT"])) { 532 $ip = $_SERVER["REMOTE_PORT"]; 533 } 534 535 return $ip; 536 } 537 } 538 539 ?>
Download modules/util/Http.class.php
History Wed, 2 Feb 2022 01:12:42 +0100 dankert Better support for API requests. Sun, 30 Jan 2022 23:38:42 +0100 dankert Refactoring: Only 1 http-endpoint for both the UI and the API. Path "/api" is not available any more, all API data is served under "/". Sat, 26 Sep 2020 12:20:43 +0200 Jan Dankert Refactoring: No global variables like $SESS any more. All constants are capsulated by classes. Sat, 26 Sep 2020 02:26:39 +0200 Jan Dankert Refactoring: No global functions any more, the database object is read from the Db class. Sun, 23 Feb 2020 00:03:40 +0100 Jan Dankert Refactoring: Namespaces for modules 'logger' and 'language' Sat, 22 Feb 2020 23:58:02 +0100 Jan Dankert Refactoring: Namespacing for module 'util'. Tue, 18 Feb 2020 00:59:51 +0100 Jan Dankert Total refactoring: All components are creating elements. Status: Work in progress. Mon, 20 May 2019 00:34:19 +0200 Jan Dankert Refactoring: Datenbankverbindung im Dispatcher erstellen. Bisher wurde in der Loginaction die DB-Verbindung aufgebaut, was dort falsch aufgehoben war. Wed, 29 Aug 2018 22:39:00 +0200 Jan Dankert Verschönerung der Fehlermeldung im Falle eines schlimmen Fehlers. Wed, 29 Aug 2018 01:33:22 +0200 Jan Dankert Aufgeräumt: Die RenderParams werden zu beginn erzeugt und werden in den Dispatcher und die Action reingereicht. Dadurch entfällt das Durchreichen von Action/Method. Sat, 30 Dec 2017 02:32:24 +0100 Jan Dankert Schönere HTML-Serverfehlermeldung. Außerdem müssen noch an ganz vielen Stellen die HTTP-Fehler in Exceptions umgebaut werden... Wed, 20 Dec 2017 23:02:40 +0100 Jan Dankert Das Projekt erhält eine neue Spalte 'flags', in der alle möglichen Einstellungen als Bitmaske hinterlegt werden können. Außerdem Refactoring der DBUpdate-Schnittstelle, möglichst häufig Konstanten verwenden. Sat, 16 Dec 2017 23:21:31 +0100 Jan Dankert Eigenes Modul für alle Util-Klassen.