Issues (16)

src/Surfer/Adapter/CurlAdapter.php (1 issue)

Labels
Severity
1
<?php
2
3
/**
4
 * @file CurlAdapter.php
5
 * @brief This file contains the CurlAdapter class.
6
 * @details
7
 * @author Filippo F. Fadda
8
 */
9
10
11
namespace Surfer\Adapter;
12
13
14
use Surfer\Message\Request;
15
use Surfer\Message\Response;
16
use Surfer\Hook;
17
18
19
/**
20
 * @brief An HTTP 1.1 client adapter using cURL.
21
 * @nosubgrouping
22
 * @attention To use this client adapter, cURL must be installed on server.
23
 */
24
class CurlAdapter extends AbstractAdapter {
25
26
  // cURL handle.
27
  private $handle;
28
29
  // Timeout is seconds.
30
  private $timeout = NULL;
31
32
33
  /**
34
   * @copydoc AbstractAdapter::__construct()
35
   */
36
  public function __construct($server = parent::DEFAULT_SERVER, $userName = "", $password = "") {
37
    $this->initialize();
38
39
    parent::__construct($server, $userName, $password);
40
41
    // Init cURL.
42
    $this->handle = curl_init();
43
  }
44
45
46
  /**
47
   * @brief Destroys the cURL handle.
48
   */
49
  public function __destruct() {
50
    curl_close($this->handle);
51
  }
52
53
54
  /**
55
   * @copydoc AbstractAdapter::initialize()
56
   */
57
  public function initialize() {
58
    if (!extension_loaded("curl"))
59
      throw new \RuntimeException("The cURL extension is not loaded.");
60
  }
61
62
63
  /**
64
   * @copydoc AbstractAdapter::setTimeout()
65
   */
66
  public function setTimeout($seconds) {
67
    $this->timeout = (int)$seconds;
68
  }
69
70
71
  /**
72
   * @copydoc AbstractAdapter::send()
73
   * @bug https://github.com/dedalozzo/eoc-client/issues/2
74
   */
75
  public function send(Request $request, Hook\IChunkHook $chunkHook = NULL) {
76
    $opts = [];
77
78
    // Resets all the cURL options. The curl_reset() function is available only since PHP 5.5.
79
    if (function_exists('curl_reset'))
80
      curl_reset($this->handle);
81
82
    // Sets the methods and its related options.
83
    switch ($request->getMethod()) {
84
85
      // GET method.
86
      case Request::GET_METHOD:
87
        $opts[CURLOPT_HTTPGET] = TRUE;
88
        break;
89
90
      // POST method.
91
      case Request::POST_METHOD:
92
        $opts[CURLOPT_POST] = TRUE;
93
94
        // The full data to post in a HTTP "POST" operation. To post a file, prepend a filename with @ and use the full
95
        // path. This can either be passed as a urlencoded string like 'para1=val1&para2=val2&...' or as an array with
96
        // the field name as key and field data as value. If value is an array, the Content-Type header will be set to
97
        // multipart/form-data.
98
        $opts[CURLOPT_POSTFIELDS] = $request->getBody();
99
        break;
100
101
      // PUT method.
102
      case Request::PUT_METHOD:
103
        $opts[CURLOPT_PUT] = TRUE;
104
105
        // Often a request contains data in the form of a JSON object. Since cURL is just able to read data from a file,
106
        // but we can't create a temporary file because it's a too much expensive operation, the code below uses a faster
107
        // and efficient memory stream.
108
        if ($request->hasBody()) {
109
          if ($fd = fopen("php://memory", "r+")) { // Try to create a temporary file in memory.
110
            fputs($fd, $request->getBody()); // Writes the message body.
111
            rewind($fd); // Sets the pointer to the beginning of the file stream.
112
113
            $opts[CURLOPT_INFILE] = $fd;
114
            $opts[CURLOPT_INFILESIZE] = $request->getBodyLength();
115
          }
116
          else
117
            throw new \RuntimeException("Cannot create the stream.");
118
        }
119
120
        break;
121
122
      // DELETE method.
123
      case Request::DELETE_METHOD:
124
        $opts[CURLOPT_CUSTOMREQUEST] = Request::DELETE_METHOD;
125
        break;
126
127
      // COPY or any other custom method.
128
      default:
129
        $opts[CURLOPT_CUSTOMREQUEST] = $request->getMethod();
130
131
    } // switch
132
133
    // Sets the request Uniform Resource Locator.
134
    $opts[CURLOPT_URL] = $this->scheme.$this->host.":".$this->port.$request->getPath().$request->getQueryString();
135
136
    // Includes the header in the output. We need this because our Response object will parse them.
137
    // NOTE: we don't include header anymore, because we use the option CURLOPT_HEADERFUNCTION.
138
    //$opts[CURLOPT_HEADER] = TRUE;
139
140
    // Returns the transfer as a string of the return value of curl_exec() instead of outputting it out directly.
141
    $opts[CURLOPT_RETURNTRANSFER] = TRUE;
142
143
    // Sets the protocol version to be used. cURL constants have different values.
144
    $opts[CURLOPT_HTTP_VERSION] = CURL_HTTP_VERSION_1_1;
145
146
    // Sets basic authentication.
147
    if (!empty($this->userName)) {
148
      $opts[CURLOPT_HTTPAUTH] = CURLAUTH_BASIC;
149
      $opts[CURLOPT_USERPWD] = $this->userName.":".$this->password;
150
    }
151
152
    // Sets the previous options.
153
    curl_setopt_array($this->handle, $opts);
154
155
    // This fix a known cURL bug: see http://the-stickman.com/web-development/php-and-curl-disabling-100-continue-header/
156
    // cURL sets the Expect header field automatically, ignoring the fact that a client may not need it for the specific
157
    // request.
158
    if (!$request->hasHeaderField(Request::EXPECT_HF))
159
      curl_setopt($this->handle, CURLOPT_HTTPHEADER, array("Expect:"));
160
161
    // Sets the request header.
162
    // Due to a stupid bug, using curl_setopt_array(), cURL doesn't override the Content-Type header field. So we must
163
    // set the header using, instead, curl_stopt()
164
    // $opts[CURLOPT_HTTPHEADER] = $request->getHeaderAsArray();
165
    curl_setopt($this->handle, CURLOPT_HTTPHEADER, $request->getHeaderAsArray());
166
167
    // Here we use this option because we might have a response without body. This may happen because we are supporting
168
    // chunk responses, and sometimes we want trigger an hook function to let the user perform operations on coming
169
    // chunks.
170
    $header = "";
171
    curl_setopt($this->handle, CURLOPT_HEADERFUNCTION,
172
      function($unused, $buffer) use (&$header) {
173
        $header .= $buffer;
174
        return strlen($buffer);
175
      });
176
177
    // When the hook function is provided, we set the CURLOPT_WRITEFUNCTION so cURL will call the hook function for each
178
    // response chunk read.
179
    if (isset($chunkHook)) {
180
      curl_setopt($this->handle, CURLOPT_WRITEFUNCTION,
181
        function($unused, $buffer) use ($chunkHook) {
182
          $chunkHook->process($buffer);
183
          return strlen($buffer);
184
        });
185
    }
186
187
    if ($this->timeout)
188
      curl_setopt($this->handle, CURLOPT_TIMEOUT, $this->timeout);
189
190
    if ($result = curl_exec($this->handle)) {
191
      $response = new Response($header);
192
      $response->setBody($result);
0 ignored issues
show
It seems like $result can also be of type true; however, parameter $body of Surfer\Message\Message::setBody() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

192
      $response->setBody(/** @scrutinizer ignore-type */ $result);
Loading history...
193
      return $response;
194
    }
195
    else {
196
      $error = curl_error($this->handle);
197
      throw new \RuntimeException($error);
198
    }
199
  }
200
201
}