fetch.php 8.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274
  1. <?php
  2. /**
  3. * This file contain fetch class, which is used to communicate with NTFY.sh
  4. * REST api. It makes that HTTP request using buildin CURL extension is
  5. * much easier, and looks better in high level code.
  6. *
  7. * @package phpnotify
  8. * @author Cixo (Cixo Electronic)
  9. */
  10. namespace phpnotify;
  11. use \TypeError as TypeError;
  12. use \RuntimeException as RuntimeException;
  13. use \CurlHandle as CurlHandle;
  14. use \curl_init as curl_init;
  15. use \curl_setopt as curl_setopt;
  16. use \curl_close as curl_close;
  17. use \curl_exec as curl_exec;
  18. use \curl_error as curl_error;
  19. use \curl_errno as curl_errno;
  20. /**
  21. * This class is responsible for downloading data from the server. It uses
  22. * buildin CURL extension to make HTTP request. It not parsing response
  23. * directly, but use {@link response}
  24. */
  25. class fetch {
  26. /**
  27. * This store content of the requst body.
  28. * @var ?string
  29. */
  30. private ?string $content;
  31. /**
  32. * This store type of content.
  33. * @var ?string
  34. */
  35. private ?string $content_type;
  36. /**
  37. * This store method of the request.
  38. * @var string
  39. */
  40. private string $method;
  41. /**
  42. * This store url to fetch.
  43. * @var string
  44. */
  45. private string $url;
  46. /**
  47. * This store headers of the request.
  48. * @var array
  49. */
  50. private array $headers;
  51. /**
  52. * This create new fetch request. It is alias of constructor.
  53. *
  54. * @link fetch::__construct()
  55. * @static
  56. * @param string $url URL to fetch.
  57. * @return fetch New fetch object.
  58. */
  59. public static function create(string $url): object {
  60. return new self($url);
  61. }
  62. /**
  63. * This initialize new fetch request by given URL. Default method for
  64. * the request is GET. If do not want to use new, see create.
  65. *
  66. * @link fetch::create()
  67. * @param string $url URL to work with.
  68. */
  69. public function __construct(string $url) {
  70. $this->content = null;
  71. $this->content_type = null;
  72. $this->method = 'GET';
  73. $this->headers = array();
  74. $this->url = $url;
  75. }
  76. /**
  77. * This function could be use send array in the fetch request. Be careful
  78. * because this function verify that method of the request is not GET or
  79. * HEAD, which can not contain body. Property method must be set before.
  80. *
  81. * @param array $content Content which must be send.
  82. * @return fetch Itself to chain load.
  83. */
  84. public function send_array(array $content): object {
  85. $content = json_encode($content);
  86. $type = 'application/json';
  87. $this->send_raw($content, $type);
  88. return $this;
  89. }
  90. /**
  91. * This function is similar to {@link fetch::send_array()}, but work on
  92. * raw string data. Also content type must be set. Be careful, because
  93. * it check that method of the request is not GET or HEAD, which can
  94. * not contain body content.
  95. *
  96. * @param string $content Content of the body to send.
  97. * @param string $type Type of the content, like "text/plain".
  98. * @return fetch Self to chan loading.
  99. */
  100. public function send_raw(string $content, string $type): object {
  101. if ($this->method === 'GET' or $this->method === 'HEAD') {
  102. throw new TypeError('GET or HEAD request can not contain body.');
  103. }
  104. $this->content = $content;
  105. $this->content_type = $type;
  106. return $this;
  107. }
  108. /**
  109. * That add new header for the request. Name of the header must be full
  110. * featured header name, like 'Cache-Control'. It also had been validated
  111. * and when name had been blacklisted, it raise RuntimeException. It also
  112. * raise RuntimeException when name has any white chars. Content had been
  113. * validating, and it could not have new lines. Name and content would be
  114. * trimed before processing. Header could not already exists.
  115. *
  116. * Blacklisted headers:
  117. * Content-Type, Content-Length
  118. *
  119. * @throws RuntimeException When header already exists.
  120. * @throws RuntimeException When header is blacklisted.
  121. * @throws RuntimeException When header contain blocked chars.
  122. *
  123. * @param string $name Name of the header.
  124. * @param string $content Content of the header.
  125. * @return fetch Self to chain processing.
  126. */
  127. public function add_header(string $name, string $content): object {
  128. if (array_key_exists(trim($name), $this->headers)) {
  129. throw new RuntimeExcetion('"'.$name.'" header already exists.');
  130. }
  131. return $this->set_header($name, $content);
  132. }
  133. /**
  134. * That set header for the request. Name of the header must be full
  135. * featured header name, like 'Cache-Control'. It also had been validated
  136. * and when name had been blacklisted, it raise RuntimeException. It also
  137. * raise RuntimeException when name has any white chars. Content had been
  138. * validating, and it could not have new lines. Name and content would be
  139. * trimed before processing.
  140. *
  141. * Blacklisted headers:
  142. * Content-Type, Content-Length.
  143. *
  144. * @throws RuntimeException When header is blacklisted.
  145. * @throws RuntimeException When header contain blocked chars.
  146. *
  147. * @param string $name Name of the header.
  148. * @param string $content Content of the header.
  149. * @return fetch Self to chain processing.
  150. */
  151. public function set_header(string $name, string $content): object {
  152. $name = trim($name);
  153. $content = trim($content);
  154. if (strpos($name, ' ') !== false) {
  155. throw new RuntimeException(
  156. 'Header "'.$name.'" contains white char.'
  157. );
  158. }
  159. $check_content = strpos($content, "\r") !== false;
  160. $check_content = $check_content || strpos($content, "\n") !== false;
  161. if ($check_content) {
  162. throw new RuntimeException(
  163. 'Header content "'.$content.'" contains invalid white char.'
  164. );
  165. }
  166. if ($name === 'Content-Type') {
  167. throw new RuntimeException('Content-Type header is automatic.');
  168. }
  169. if ($name === 'Content-Lenght') {
  170. throw new RuntimeException('Content-Lenght header is automatic.');
  171. }
  172. $this->headers[$name] = $name.': '.$content;
  173. return $this;
  174. }
  175. /**
  176. * This function set method of the request. Be careful, because it check
  177. * that any content is not set, and when content is not null, but trying
  178. * to set GET or HEAD method, it throw TypeError.
  179. *
  180. * @param string $name Name of the method.
  181. * @return fetch Self to chain loading.
  182. */
  183. public function set_method(string $name): object {
  184. if ($this->content === null) {
  185. $this->method = $name;
  186. return $this;
  187. }
  188. if ($name === 'GET' || $name === 'HEAD') {
  189. throw new TypeError('GET or HEAD request can not contain body.');
  190. }
  191. $this->method = $name;
  192. return $this;
  193. }
  194. /**
  195. * This return list of all headers for the request.
  196. *
  197. * @return array List of the all headers.
  198. */
  199. private function get_headers(): array {
  200. return array_values($this->headers);
  201. }
  202. /**
  203. * That function make request to the server, from fetch config, which
  204. * had been config previously. It return response in parsed form, as
  205. * instance of response class. Be careful, it raise an RuntimeException
  206. * when server does not response correctly.
  207. *
  208. * @return response Response from the server.
  209. */
  210. public function request(): response {
  211. $headers = $this->get_headers();
  212. $request = curl_init($this->url);
  213. curl_setopt($request, CURLOPT_RETURNTRANSFER, true);
  214. curl_setopt($request, CURLOPT_FAILONERROR, true);
  215. curl_setopt($request, CURLOPT_HEADER, true);
  216. curl_setopt($request,CURLOPT_CUSTOMREQUEST, $this->method);
  217. if ($this->content !== null) {
  218. array_push($headers, 'Content-Type: '.$this->content_type);
  219. curl_setopt($request, CURLOPT_POSTFIELDS, $this->content);
  220. }
  221. curl_setopt($request, CURLOPT_HTTPHEADER, $headers);
  222. $result = curl_exec($request);
  223. $error = curl_error($request);
  224. $error_number = curl_errno($request);
  225. curl_close($request);
  226. if ($result !== false and $error_number === 0) {
  227. return new response($result);
  228. }
  229. throw new RuntimeException(
  230. 'Can not fetch request. Error code '
  231. .strval($error_number)
  232. .': "'.$error.'".'
  233. );
  234. }
  235. }