1 <?php
  2 /**
  3  * @package     Joomla.Platform
  4  * @subpackage  HTTP
  5  *
  6  * @copyright   Copyright (C) 2005 - 2017 Open Source Matters, Inc. All rights reserved.
  7  * @license     GNU General Public License version 2 or later; see LICENSE
  8  */
  9 
 10 defined('JPATH_PLATFORM') or die;
 11 
 12 use Joomla\Registry\Registry;
 13 
 14 /**
 15  * HTTP transport class for using PHP streams.
 16  *
 17  * @since  11.3
 18  */
 19 class JHttpTransportStream implements JHttpTransport
 20 {
 21     /**
 22      * @var    Registry  The client options.
 23      * @since  11.3
 24      */
 25     protected $options;
 26 
 27     /**
 28      * Constructor.
 29      *
 30      * @param   Registry  $options  Client options object.
 31      *
 32      * @since   11.3
 33      * @throws  RuntimeException
 34      */
 35     public function __construct(Registry $options)
 36     {
 37         // Verify that URLs can be used with fopen();
 38         if (!ini_get('allow_url_fopen'))
 39         {
 40             throw new RuntimeException('Cannot use a stream transport when "allow_url_fopen" is disabled.');
 41         }
 42 
 43         // Verify that fopen() is available.
 44         if (!self::isSupported())
 45         {
 46             throw new RuntimeException('Cannot use a stream transport when fopen() is not available or "allow_url_fopen" is disabled.');
 47         }
 48 
 49         $this->options = $options;
 50     }
 51 
 52     /**
 53      * Send a request to the server and return a JHttpResponse object with the response.
 54      *
 55      * @param   string   $method     The HTTP method for sending the request.
 56      * @param   JUri     $uri        The URI to the resource to request.
 57      * @param   mixed    $data       Either an associative array or a string to be sent with the request.
 58      * @param   array    $headers    An array of request headers to send with the request.
 59      * @param   integer  $timeout    Read timeout in seconds.
 60      * @param   string   $userAgent  The optional user agent string to send with the request.
 61      *
 62      * @return  JHttpResponse
 63      *
 64      * @since   11.3
 65      * @throws  RuntimeException
 66      */
 67     public function request($method, JUri $uri, $data = null, array $headers = null, $timeout = null, $userAgent = null)
 68     {
 69         // Create the stream context options array with the required method offset.
 70         $options = array('method' => strtoupper($method));
 71 
 72         // If data exists let's encode it and make sure our Content-Type header is set.
 73         if (isset($data))
 74         {
 75             // If the data is a scalar value simply add it to the stream context options.
 76             if (is_scalar($data))
 77             {
 78                 $options['content'] = $data;
 79             }
 80             // Otherwise we need to encode the value first.
 81             else
 82             {
 83                 $options['content'] = http_build_query($data);
 84             }
 85 
 86             if (!isset($headers['Content-Type']))
 87             {
 88                 $headers['Content-Type'] = 'application/x-www-form-urlencoded; charset=utf-8';
 89             }
 90 
 91             // Add the relevant headers.
 92             $headers['Content-Length'] = strlen($options['content']);
 93         }
 94 
 95         // If an explicit timeout is given user it.
 96         if (isset($timeout))
 97         {
 98             $options['timeout'] = (int) $timeout;
 99         }
100 
101         // If an explicit user agent is given use it.
102         if (isset($userAgent))
103         {
104             $options['user_agent'] = $userAgent;
105         }
106 
107         // Ignore HTTP errors so that we can capture them.
108         $options['ignore_errors'] = 1;
109 
110         // Follow redirects.
111         $options['follow_location'] = (int) $this->options->get('follow_location', 1);
112 
113         // Set any custom transport options
114         foreach ($this->options->get('transport.stream', array()) as $key => $value)
115         {
116             $options[$key] = $value;
117         }
118 
119         // Add the proxy configuration, if any.
120         $config = JFactory::getConfig();
121 
122         if ($config->get('proxy_enable'))
123         {
124             $options['proxy'] = $config->get('proxy_host') . ':' . $config->get('proxy_port');
125             $options['request_fulluri'] = true;
126 
127             // Put any required authorization into the headers array to be handled later
128             // TODO: do we need to support any auth type other than Basic?
129             if ($user = $config->get('proxy_user'))
130             {
131                 $auth = base64_encode($config->get('proxy_user') . ':' . $config->get('proxy_pass'));
132 
133                 $headers['Proxy-Authorization'] = 'Basic ' . $auth;
134             }
135         }
136 
137         // Build the headers string for the request.
138         $headerEntries = array();
139 
140         if (isset($headers))
141         {
142             foreach ($headers as $key => $value)
143             {
144                 $headerEntries[] = $key . ': ' . $value;
145             }
146 
147             // Add the headers string into the stream context options array.
148             $options['header'] = implode("\r\n", $headerEntries);
149         }
150 
151         // Get the current context options.
152         $contextOptions = stream_context_get_options(stream_context_get_default());
153 
154         // Add our options to the current ones, if any.
155         $contextOptions['http'] = isset($contextOptions['http']) ? array_merge($contextOptions['http'], $options) : $options;
156 
157         // Create the stream context for the request.
158         $context = stream_context_create(
159             array(
160                 'http' => $options,
161                 'ssl' => array(
162                     'verify_peer'   => true,
163                     'cafile'        => $this->options->get('stream.certpath', __DIR__ . '/cacert.pem'),
164                     'verify_depth'  => 5,
165                 ),
166             )
167         );
168 
169         // Authentification, if needed
170         if ($this->options->get('userauth') && $this->options->get('passwordauth'))
171         {
172             $uri->setUser($this->options->get('userauth'));
173             $uri->setPass($this->options->get('passwordauth'));
174         }
175 
176         // Capture PHP errors
177         $php_errormsg = '';
178         $track_errors = ini_get('track_errors');
179         ini_set('track_errors', true);
180 
181         // Open the stream for reading.
182         $stream = @fopen((string) $uri, 'r', false, $context);
183 
184         if (!$stream)
185         {
186             if (!$php_errormsg)
187             {
188                 // Error but nothing from php? Create our own
189                 $php_errormsg = sprintf('Could not connect to resource: %s', $uri, $err, $errno);
190             }
191 
192             // Restore error tracking to give control to the exception handler
193             ini_set('track_errors', $track_errors);
194 
195             throw new RuntimeException($php_errormsg);
196         }
197 
198         // Restore error tracking to what it was before.
199         ini_set('track_errors', $track_errors);
200 
201         // Get the metadata for the stream, including response headers.
202         $metadata = stream_get_meta_data($stream);
203 
204         // Get the contents from the stream.
205         $content = stream_get_contents($stream);
206 
207         // Close the stream.
208         fclose($stream);
209 
210         if (isset($metadata['wrapper_data']['headers']))
211         {
212             $headers = $metadata['wrapper_data']['headers'];
213         }
214         elseif (isset($metadata['wrapper_data']))
215         {
216             $headers = $metadata['wrapper_data'];
217         }
218         else
219         {
220             $headers = array();
221         }
222 
223         return $this->getResponse($headers, $content);
224     }
225 
226     /**
227      * Method to get a response object from a server response.
228      *
229      * @param   array   $headers  The response headers as an array.
230      * @param   string  $body     The response body as a string.
231      *
232      * @return  JHttpResponse
233      *
234      * @since   11.3
235      * @throws  UnexpectedValueException
236      */
237     protected function getResponse(array $headers, $body)
238     {
239         // Create the response object.
240         $return = new JHttpResponse;
241 
242         // Set the body for the response.
243         $return->body = $body;
244 
245         // Get the response code from the first offset of the response headers.
246         preg_match('/[0-9]{3}/', array_shift($headers), $matches);
247         $code = $matches[0];
248 
249         if (is_numeric($code))
250         {
251             $return->code = (int) $code;
252         }
253 
254         // No valid response code was detected.
255         else
256         {
257             throw new UnexpectedValueException('No HTTP response code found.');
258         }
259 
260         // Add the response headers to the response object.
261         foreach ($headers as $header)
262         {
263             $pos = strpos($header, ':');
264             $return->headers[trim(substr($header, 0, $pos))] = trim(substr($header, ($pos + 1)));
265         }
266 
267         return $return;
268     }
269 
270     /**
271      * Method to check if http transport stream available for use
272      *
273      * @return bool true if available else false
274      *
275      * @since   12.1
276      */
277     public static function isSupported()
278     {
279         return function_exists('fopen') && is_callable('fopen') && ini_get('allow_url_fopen');
280     }
281 }
282