curl.php 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350
  1. <?php namespace Lib;
  2. /**
  3. * Defines an unexpected response.
  4. *
  5. * @author Joachim M. Giaever (joachim[]giaever.org)
  6. * @package curl
  7. * @version 0.1
  8. */
  9. class HTTPUnexpectedResponse extends \Exception {
  10. /**
  11. * Includes valid codes, as a valid code can also be unexpeted.
  12. *
  13. * @var array $ecode HTTP status codes
  14. * @static
  15. */
  16. static $ecode = array(
  17. 0 => "Unknown error",
  18. 100 => 'Continue',
  19. 101 => 'Switching Protocols',
  20. 102 => 'Processing', // WebDAV; RFC 2518
  21. 200 => 'OK',
  22. 201 => 'Created',
  23. 202 => 'Accepted',
  24. 203 => 'Non-Authoritative Information', // since HTTP/1.1
  25. 204 => 'No Content',
  26. 205 => 'Reset Content',
  27. 206 => 'Partial Content',
  28. 207 => 'Multi-Status', // WebDAV; RFC 4918
  29. 208 => 'Already Reported', // WebDAV; RFC 5842
  30. 226 => 'IM Used', // RFC 3229
  31. 300 => 'Multiple Choices',
  32. 301 => 'Moved Permanently',
  33. 302 => 'Found',
  34. 303 => 'See Other', // since HTTP/1.1
  35. 304 => 'Not Modified',
  36. 305 => 'Use Proxy', // since HTTP/1.1
  37. 306 => 'Switch Proxy',
  38. 307 => 'Temporary Redirect', // since HTTP/1.1
  39. 308 => 'Permanent Redirect', // approved as experimental RFC
  40. 400 => 'Bad Request',
  41. 401 => 'Unauthorized',
  42. 402 => 'Payment Required',
  43. 403 => 'Forbidden',
  44. 404 => 'Not Found',
  45. 405 => 'Method Not Allowed',
  46. 406 => 'Not Acceptable',
  47. 407 => 'Proxy Authentication Required',
  48. 408 => 'Request Timeout',
  49. 409 => 'Conflict',
  50. 410 => 'Gone',
  51. 411 => 'Length Required',
  52. 412 => 'Precondition Failed',
  53. 413 => 'Request Entity Too Large',
  54. 414 => 'Request-URI Too Long',
  55. 415 => 'Unsupported Media Type',
  56. 416 => 'Requested Range Not Satisfiable',
  57. 417 => 'Expectation Failed',
  58. 418 => 'I\'m a teapot', // RFC 2324
  59. 419 => 'Authentication Timeout', // not in RFC 2616
  60. 420 => 'Enhance Your Calm', // Twitter
  61. 420 => 'Method Failure', // Spring Framework
  62. 422 => 'Unprocessable Entity', // WebDAV; RFC 4918
  63. 423 => 'Locked', // WebDAV; RFC 4918
  64. 424 => 'Failed Dependency', // WebDAV; RFC 4918
  65. 424 => 'Method Failure', // WebDAV)
  66. 425 => 'Unordered Collection', // Internet draft
  67. 426 => 'Upgrade Required', // RFC 2817
  68. 428 => 'Precondition Required', // RFC 6585
  69. 429 => 'Too Many Requests', // RFC 6585
  70. 431 => 'Request Header Fields Too Large', // RFC 6585
  71. 444 => 'No Response', // Nginx
  72. 449 => 'Retry With', // Microsoft
  73. 450 => 'Blocked by Windows Parental Controls', // Microsoft
  74. 451 => 'Redirect', // Microsoft
  75. 451 => 'Unavailable For Legal Reasons', // Internet draft
  76. 494 => 'Request Header Too Large', // Nginx
  77. 495 => 'Cert Error', // Nginx
  78. 496 => 'No Cert', // Nginx
  79. 497 => 'HTTP to HTTPS', // Nginx
  80. 499 => 'Client Closed Request', // Nginx
  81. 500 => 'Internal Server Error',
  82. 501 => 'Not Implemented',
  83. 502 => 'Bad Gateway',
  84. 503 => 'Service Unavailable',
  85. 504 => 'Gateway Timeout',
  86. 505 => 'HTTP Version Not Supported',
  87. 506 => 'Variant Also Negotiates', // RFC 2295
  88. 507 => 'Insufficient Storage', // WebDAV; RFC 4918
  89. 508 => 'Loop Detected', // WebDAV; RFC 5842
  90. 509 => 'Bandwidth Limit Exceeded', // Apache bw/limited extension
  91. 510 => 'Not Extended', // RFC 2774
  92. 511 => 'Network Authentication Required', // RFC 6585
  93. 598 => 'Network read timeout error', // Unknown
  94. 599 => 'Network connect timeout error', // Unknown
  95. );
  96. /**
  97. * The response from server (body)
  98. * @access private
  99. */
  100. private $response;
  101. /**
  102. * Sets the exceptions.
  103. *
  104. * @string $message - the response from the server.
  105. * @string $code - the HTTP status code.
  106. * @exception $prev - Previous exceptions
  107. **/
  108. public function __construct(string $message, int $code = 0, Exception $previous = null) {
  109. $this->response = $message;
  110. parent::__construct(HTTPUnexpectedResponse::$ecode[$code], $code, $previous);
  111. }
  112. /**
  113. * Visual representation of the exception.
  114. *
  115. * @return string
  116. */
  117. public function __toString() {
  118. return __CLASS__ . ": [{$this->code} | {$this->message}]: {$this->response}\n";
  119. }
  120. /**
  121. * Get the actual response from the body or the request.
  122. *
  123. * @return string
  124. */
  125. public function getResponse() {
  126. return $this->response;
  127. }
  128. }
  129. /**
  130. * When the request fails because of an unauthorized token,
  131. * this is thrown instead.
  132. *
  133. * @author Joachim M. Giaever (joachim[]giaever.org)
  134. * @package curl
  135. * @version 0.1
  136. */
  137. class NotAuthorizedException extends HTTPUnexpectedResponse {
  138. /**
  139. * Sets the exceptions.
  140. *
  141. * @string $message - the response from the server.
  142. * @string $code - the HTTP status code, @default 401
  143. * @exception $prev - Previous exceptions
  144. **/
  145. public function __construct($message, $code = 401, Exception $previous = null) {
  146. parent::__construct($message, $code, $previous);
  147. }
  148. }
  149. /**
  150. * A trait used for every class referencing the api-url and token.
  151. *
  152. * @author Joachim M. Giaever (joachim[]giaever.org)
  153. * @package curl
  154. * @version 0.1
  155. */
  156. trait Curl {
  157. protected $url;
  158. protected $token;
  159. // TODO: Change this to something!
  160. protected $user_agent = "Gogs PHP Api Client/0.1 (compatible; LINUX)";
  161. protected $timeout = 30;
  162. protected $max_redirects = 4;
  163. /**
  164. * array_2_params takes an array and converts it into a
  165. * query string (e.g param=val&param2=val2).
  166. *
  167. * @param array $params parameters to pass
  168. * @return string
  169. */
  170. private function array_2_params(array $params) {
  171. return join("&", array_map(function($k, $v) {
  172. return sprintf("%s=%s", $k, rawurlencode(is_bool($v) ? ($v ? "true" : "false") : $v ));
  173. }, array_keys($params), $params));
  174. }
  175. /**
  176. * array_2_json takes an array and converts it into a
  177. * json-string (e.g {'name': 'This'}) which is typically
  178. * used in a request body.
  179. *
  180. * @param array $params paramters to pass
  181. * @return string
  182. */
  183. private function array_2_json(array $params) {
  184. return count($params) == 0 ? null : json_encode($params);
  185. }
  186. /**
  187. * Initializes a curl request of different kinds, depending
  188. * on the specified method. This can be
  189. *
  190. * DELETE, PATCH, POST or GET. An unidentified value will
  191. * become a GET-request.
  192. *
  193. * @param string $method either DELETE, PATCH, POST, GET
  194. * @param string &$req variable to store request body in
  195. * @param string $scope scope within the API (e.g /user/repos)
  196. * @param array $params parameters to pass
  197. * @param bool $ret return transfer
  198. * @return int the status code
  199. */
  200. protected function method(string $method, string &$req, string $scope, array $params, bool $ret) {
  201. $c = curl_init();
  202. if (!$c) {
  203. return false;
  204. }
  205. $headers = array(
  206. sprintf("Authorization: token %s", $this->token),
  207. );
  208. $url = sprintf("%s%s", $this->url, $scope);
  209. echo sprintf("%s: %s, params: %s\n", $method, $url, $this->array_2_json($params));
  210. if (in_array($method, array("DELETE", "PATCH", "POST"))) {
  211. $json = $this->array_2_json($params);
  212. curl_setopt($c, CURLOPT_CUSTOMREQUEST, $method);
  213. curl_setopt($c, CURLOPT_POSTFIELDS, $json);
  214. array_unshift($headers, "Content-Type: application/json");
  215. array_push($headers, "Content-Length: " . strlen($json));
  216. } else {
  217. $url .= "?" . $this->array_2_params($params);
  218. }
  219. curl_setopt($c, CURLOPT_USERAGENT, $this->user_agent);
  220. curl_setopt($c, CURLOPT_URL, $url);
  221. curl_setopt($c, CURLOPT_HTTPHEADER, $headers);
  222. curl_setopt($c, CURLOPT_RETURNTRANSFER, $ret);
  223. curl_setopt($c, CURLOPT_TIMEOUT, $this->timeout);
  224. curl_setopt($c, CURLOPT_MAXREDIRS, $this->max_redirects);
  225. curl_setopt($c, CURLOPT_FOLLOWLOCATION, true);
  226. $req = curl_exec($c);
  227. $status_code = curl_getinfo($c, CURLINFO_HTTP_CODE);
  228. curl_close($c);
  229. return $status_code;
  230. }
  231. /**
  232. * Checks if the user is authorized for the scope. Shouldn't
  233. * be used frequently. One test for one scope should be enough,
  234. * but if you know for sure thats you're programming with the
  235. * use of an authorized user you should leave this and just
  236. * handle the NotAuthorizedExeption whenever thrown.
  237. *
  238. * @param $scope the scope, a relative uri.
  239. * @throws Not AuthorizedException if server responde with a 401
  240. * @return bool
  241. */
  242. protected function authorized(string $scope = "") {
  243. $ret = "";
  244. if ($this->method("GET", $ret, $scope, array(), false) == 401) {
  245. throw new NotAuthorizedException("Not authorized", 401);
  246. }
  247. return true;
  248. }
  249. /**
  250. * Post method.
  251. *
  252. * @param string $scope the scope, a relative uri.
  253. * @param array $params the parameters to post.
  254. * @throws NotAuthorizedException on 401, 403
  255. * @throws HTTPUnexpectedResponse when not 200,201,401,403
  256. * @return string the request content.
  257. */
  258. private function post(string $scope = "", array $params = array()) {
  259. $req = "";
  260. $code = $this->method("POST", $req, $scope, $params, true);
  261. switch ($code) {
  262. case 200:
  263. case 201:
  264. return $req;
  265. case 401:
  266. case 403:
  267. throw new NotAuthorizedException($req, $code);
  268. default:
  269. throw new HTTPUnexpectedResponse($req, $code);
  270. }
  271. }
  272. /**
  273. * Delete method.
  274. *
  275. * @param string $scope the scope, a relative uri.
  276. * @throws NotAuthorizedException on 401, 403
  277. * @throws HTTPUnexpectedResponse when not 200,204,401,403
  278. * @return string the request content.
  279. */
  280. private function delete(string $scope = "") {
  281. $req = "";
  282. $code = $this->method("DELETE", $req, $scope, array(), true);
  283. switch ($code) {
  284. case 200:
  285. case 204:
  286. return $req;
  287. case 401:
  288. case 403:
  289. throw new NotAuthorizedException($req, $code);
  290. default:
  291. throw new HTTPUnexpectedResponse($req, $code);
  292. }
  293. }
  294. /**
  295. * GET method.
  296. *
  297. * @param string $scope the scope, a relative uri.
  298. * @param array $params the parameters to post.
  299. * @throws NotAuthorizedException on 401, 403
  300. * @throws HTTPUnexpectedResponse when not 200,401,403
  301. * @return string the request content.
  302. */
  303. private function get($scope = "", $params = array()) {
  304. $req = "";
  305. $code = $this->method("GET", $req, $scope, $params, true);
  306. switch ($code) {
  307. case 200:
  308. return $req;
  309. case 401:
  310. case 403:
  311. throw new NotAuthorizedException($req, $code);
  312. default:
  313. throw new HTTPUnexpectedResponse($req, $code);
  314. }
  315. }
  316. }