callbacks['onRequestLog'] = $opts['onRequestLog']; } if (!empty($opts['onResponseLog']) && ($opts['onResponseLog'] instanceof Closure)) { $this->callbacks['onResponseLog'] = $opts['onResponseLog']; } if (isset($opts['silence_fopen_warning'])) { $this->silenceFopenWarning((bool)$opts['silence_fopen_warning']); } if (isset($opts['raise_fopen_exception'])) { $this->raiseFopenException((bool)$opts['raise_fopen_exception']); } } /** * retrieve the last request we sent * * valid keys are ['url', 'method', 'querydata', 'headers', 'options', 'opts'] * * @param string $key just retrieve a given field from the hash * @return mixed */ public function getLastRequest($key=null) { if (!isset($key)) { return $this->last_request; } return $this->last_request[$key]; } /** * retrieve the last response we got * * valid keys are ['meta', 'status', 'headers', 'body'] * * @param string $key just retrieve a given field from the hash * @return mixed */ public function getLastResponse($key=null) { if (!isset($key)) { return $this->last_response; } return $this->last_response[$key]; } /** * make a GET request * * @param string the URL. This will be appended to the base_url, if any set * @param array $querydata hash of key/val pairs * @param array $headers hash of key/val pairs * @param array $options hash of key/val pairs ('timeout') * @return array the response hash * @see Resty::sendRequest() */ public function get($url, $querydata=null, $headers=null, $options=null) { return $this->sendRequest($url, 'GET', $querydata, $headers, $options); } /** * make a POST request * * @param string the URL. This will be appended to the base_url, if any set * @param array $querydata hash of key/val pairs * @param array $headers hash of key/val pairs * @param array $options hash of key/val pairs ('timeout') * @return array the response hash * @see Resty::sendRequest() */ public function post($url, $querydata=null, $headers=null, $options=null) { return $this->sendRequest($url, 'POST', $querydata, $headers, $options); } /** * make a PUT request * * @param string the URL. This will be appended to the base_url, if any set * @param array $querydata hash of key/val pairs * @param array $headers hash of key/val pairs * @param array $options hash of key/val pairs ('timeout') * @return array the response hash * @see Resty::sendRequest() */ public function put($url, $querydata=null, $headers=null, $options=null) { return $this->sendRequest($url, 'PUT', $querydata, $headers, $options); } /** * make a DELETE request * * @param string the URL. This will be appended to the base_url, if any set * @param array $querydata hash of key/val pairs * @param array $headers hash of key/val pairs * @param array $options hash of key/val pairs ('timeout') * @return array the response hash * @see Resty::sendRequest() */ public function delete($url, $querydata=null, $headers=null, $options=null) { return $this->sendRequest($url, 'DELETE', $querydata, $headers, $options); } /** * @param string $url * @param array $files * @param array $params * @param array $headers * @param array $options * * The $files array should be a set of key/val pairs, with the key being * the field name, and the val the file path. ex: * $files['avatar'] = '/path/to/file.jpg'; * $files['background'] = '/path/to/file2.jpg'; * */ public function postFiles($url, $files, $params=null, $headers=null, $options=null) { $datastr = ""; $boundary = "---------------------".substr(md5(rand(0,32000)), 0, 10); // build params if (isset($params)) { foreach($params as $key => $val) { $datastr .= "--$boundary\n"; $datastr .= "Content-Disposition: form-data; name=\"".$key."\"\n\n".$val."\n"; } } $datastr .= "--$boundary\n"; // build files foreach($files as $key => $file) { $filename = pathinfo($file, PATHINFO_BASENAME); $content_type = $this->getMimeType($file); $fileContents = file_get_contents($file); $datastr .= "Content-Disposition: form-data; name=\"{$key}\"; filename=\"{$filename}\"\n"; $datastr .= "Content-Type: {$content_type}\n"; $datastr .= "Content-Transfer-Encoding: binary\n\n"; $datastr .= $fileContents."\n"; $datastr .= "--$boundary\n"; } if (!isset($headers)) { $headers = array(); } $headers['Content-Type'] = 'multipart/form-data; boundary='.$boundary; return $this->post($url, $datastr, $headers, $options); } /** * @param string $url * @param array $binary_data * @param array $params * @param array $headers * @param array $options * * The $binary_data array should be a set of key/val pairs, with the key being * the field name, and the val the binary data. ex: * $files['avatar'] = ; * $files['background'] = ; * * with that data, a multipart POST body is created, identical to a file * upload, just without reading the data from a file * */ public function postBinary($url, $binary_data, $params=null, $headers=null, $options=null) { $datastr = ""; $boundary = "---------------------".substr(md5(rand(0,32000)), 0, 10); // build params if (isset($params)) { foreach($params as $key => $val) { $datastr .= "--$boundary\n"; $datastr .= "Content-Disposition: form-data; name=\"".$key."\"\n\n".$val."\n"; } } $datastr .= "--$boundary\n"; // build files foreach($binary_data as $key => $bdata) { $filename = 'bdata'; $content_type = 'application/octet-stream'; $datastr .= "Content-Disposition: form-data; name=\"{$key}\"; filename=\"{$filename}\"\n"; $datastr .= "Content-Type: {$content_type}\n"; $datastr .= "Content-Transfer-Encoding: binary\n\n"; $datastr .= $bdata."\n"; $datastr .= "--$boundary\n"; } if (!isset($headers)) { $headers = array(); } $headers['Content-Type'] = 'multipart/form-data; boundary='.$boundary; return $this->post($url, $datastr, $headers, $options); } /** * @see Resty::postFiles() * * Stole this from the Amazon S3 class: * * Copyright (c) 2008, Donovan Schönknecht. All rights reserved. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: * * - Redistributions of source code must retain the above copyright notice, * this list of conditions and the following disclaimer. * - Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE * POSSIBILITY OF SUCH DAMAGE. * * Amazon S3 is a trademark of Amazon.com, Inc. or its affiliates. * */ protected function getMimeType($filepath) { if (extension_loaded('fileinfo') && isset($_ENV['MAGIC']) && ($finfo = finfo_open(FILEINFO_MIME, $_ENV['MAGIC'])) !== false) { if (($type = finfo_file($finfo, $filepath)) !== false) { // Remove the charset and grab the last content-type $type = explode(' ', str_replace('; charset=', ';charset=', $type)); $type = array_pop($type); $type = explode(';', $type); $type = trim(array_shift($type)); } finfo_close($finfo); // If anyone is still using mime_content_type() } elseif (function_exists('mime_content_type')) { $type = trim(mime_content_type($filepath)); } if ($type !== false && strlen($type) > 0) { return $type; } // Otherwise do it the old fashioned way static $exts = array( 'jpg' => 'image/jpeg', 'gif' => 'image/gif', 'png' => 'image/png', 'tif' => 'image/tiff', 'tiff' => 'image/tiff', 'ico' => 'image/x-icon', 'swf' => 'application/x-shockwave-flash', 'pdf' => 'application/pdf', 'zip' => 'application/zip', 'gz' => 'application/x-gzip', 'tar' => 'application/x-tar', 'bz' => 'application/x-bzip', 'bz2' => 'application/x-bzip2', 'txt' => 'text/plain', 'asc' => 'text/plain', 'htm' => 'text/html', 'html' => 'text/html', 'css' => 'text/css', 'js' => 'text/javascript', 'xml' => 'text/xml', 'xsl' => 'application/xsl+xml', 'ogg' => 'application/ogg', 'mp3' => 'audio/mpeg', 'wav' => 'audio/x-wav', 'avi' => 'video/x-msvideo', 'mpg' => 'video/mpeg', 'mpeg' => 'video/mpeg', 'mov' => 'video/quicktime', 'flv' => 'video/x-flv', 'php' => 'text/x-php' ); $ext = strtolower(pathInfo($filepath, PATHINFO_EXTENSION)); return isset($exts[$ext]) ? $exts[$ext] : 'application/octet-stream'; } /** * bc wrapper */ public function enableDebugging($state=false) { $this->debug($state); } /** * enable or disable debugging. If no arg passed, just returns current state * @param bool $state=null if not passed, state not changed * @return boolean the current state */ public function debug($state=null) { if (isset($state)) { $this->debug = (bool)$state; } return $this->debug; } /** * raise an exception from fopen if trying to open stream fails * @param boolean $state=null optional, set the state * @return boolean the current state */ public function raiseFopenException($state=null) { if (isset($state)) { $this->raise_fopen_exception = (bool)$state; } return $this->raise_fopen_exception; } /** * silence warnings from fopen when trying to open stream * @param boolean $state=null optional, set the state * @return boolean the current state */ public function silenceFopenWarning($state=null) { if (isset($state)) { $this->silence_fopen_warning = (bool)$state; } return $this->silence_fopen_warning; } /** * sets an alternate logging method * @param Closure $logger */ public function setLogger(Closure $logger) { $this->logger = $logger; } /** * enable or disable automatic parsing of body. default is true * @param bool $state default TRUE */ public function parseBody($state=true) { $state = (bool)$state; $this->parse_body = $state; } /** * Sets the base URL for all subsequent requests * @param string $base_url */ public function setBaseURL($base_url) { $this->base_url = $base_url; } /** * retrieves the current Resty::$base_url * @return string */ public function getBaseURL() { return $this->base_url; } /** * Sets the user-agent * @param string $user_agent */ public function setUserAgent($user_agent) { $this->user_agent = $user_agent; } /** * Gets the current user agent. if Resty::$user_agent is not set, uses a default * @return string */ public function getUserAgent() { if (empty($this->user_agent)) { $this->user_agent = 'Resty ' . static::VERSION; } return $this->user_agent; } /** * Sets credentials for http basic auth * @param string $username * @param string $password */ public function setCredentials($username, $password) { $this->username = $username; $this->password = $password; } /** * removes current credentials */ public function clearCredentials() { $this->username = null; $this->password = null; } /** * takes a set of key/val pairs and builds an array of raw header strings * * @param string $headers * @return void * @author Ed Finkler */ protected function buildHeadersArray($headers) { $str_headers = array(); foreach ($headers as $key => $value) { $str_headers[] = "{$key}: {$value}"; } return $str_headers; } /** * Extracts the headers of a response from the stream's meta data * @param array $meta * @return array */ protected function metaToHeaders($meta) { $headers = array(); if (!isset($meta['wrapper_data'])) { return $headers; } foreach ($meta['wrapper_data'] as $value) { if (strpos($value, 'HTTP') !== 0) { preg_match("|^([^:]+):\s?(.+)$|", $value, $matches); if (is_array($matches) && isset($matches[2])) { $headers[trim($matches[1])] = trim($matches[2], " \t\n\r\0\x0B\""); } } } return $headers; } /** * extracts the status code from the stream meta data * @param array $meta * @return integer */ protected function getStatusCode($meta) { $matches = array(); $status = 0; preg_match("|\s(\d\d\d)\s?|", $meta['wrapper_data'][0], $matches); if (is_array($matches) && isset($matches[1])) { $status = (int)trim($matches[1]); } return $status; } /** * Sends the HTTP request and retrieves/parses the response * * @param string $url * @param string $method * @param array $querydata OPTIONAL * @param array $headers OPTIONAL * @param array $options OPTIONAL * @return array * @author Ed Finkler */ public function sendRequest($url, $method='GET', $querydata=null, $headers=null, $options=null) { $resp = array(); if ($this->base_url) { $url = $this->base_url.$url; } // we need to supply a default content-type if (!isset($headers['Content-Type'])) { $headers['Content-Type'] = 'application/x-www-form-urlencoded'; } // by default, pass the header "Connection: close" if (!isset($headers['Connection'])) { $headers['Connection'] = 'close'; } // if we have a username and password, use it if (isset($this->username) && isset($this->password) && !isset($headers['Authorization'])) { $this->log("{$this->username}:{$this->password}"); $headers['Authorization'] = 'Basic '.base64_encode("{$this->username}:{$this->password}"); } // default timeout $timeout = isset($options['timeout']) ? $options['timeout'] : static::DEFAULT_TIMEOUT; $content = null; // if querydata is a string, just pass it as-is if (isset($querydata) && is_string($querydata)) { $content = $querydata; // else if it's an array, make an http query } elseif (isset($querydata) && is_array($querydata)) { $content = http_build_query($querydata); } // create an array of header strings from the hash $headerarr = isset($headers) ? $this->buildHeadersArray($headers) : array(); // GET and DELETE should use the URL to pass data $urlcontent = ('GET' === $method || 'DELETE' === $method); // if this is a GET or DELETE and we have some $content, append to URL if ($urlcontent && isset($content)) { $url .= '?'.$content; } $opts = array( 'http'=>array( 'timeout'=>$timeout, 'method'=>$method, 'content'=> (!$urlcontent) ? $content : null, 'user_agent'=>$this->getUserAgent(), 'header'=>$headerarr, 'ignore_errors'=>1 ) ); $this->log('URL ================='); $this->log($url); $this->log('METHOD ================='); $this->log($method); $this->log('QUERYDATA ================='); $this->log($querydata); $this->log('HEADERS ================='); $this->log($headers); $this->log('OPTIONS ================='); $this->log($options); $this->log('OPTS ================='); $this->log($opts); $this->last_request = compact('url', 'method', 'querydata', 'headers', 'options', 'opts'); // call custom req log callback if (!empty($this->callbacks['onRequestLog'])) { $this->callbacks['onRequestLog']($this->last_request); } $resp_data = $this->makeStreamRequest($url, $opts); $resp['meta'] = $resp_data['meta']; $resp['body'] = $resp_data['body']; $resp['error'] = $resp_data['error']; $resp['error_msg'] = $resp_data['error_msg']; $resp['status'] = $this->getStatusCode($resp['meta']); $resp['headers'] = $this->metaToHeaders($resp['meta']); $this->log($resp); $this->log("Processing response body…"); $resp = $this->processResponseBody($resp); $this->log($resp['body']); $this->last_response = $resp; // call custom resp log callback if (!empty($this->callbacks['onResponseLog'])) { $this->callbacks['onResponseLog']($this->last_response); } return $resp; } /** * opens an http stream, sends the request, and returns result * @param [type] $url [description] * @param [type] $opts [description] * @return [type] [description] */ protected function makeStreamRequest($url, $opts) { $resp_data = array( 'meta' => null, 'body' => null, 'error' => true, 'error_msg' => null, ); $context = stream_context_create($opts); $this->log("Sending…"); $start_time = microtime(true); $this->log("Opening stream…"); if ($this->silence_fopen_warning) { $stream = @fopen($url, 'r', false, $context); } else { $stream = fopen($url, 'r', false, $context); } if (!$stream) { $req_time = static::calc_time_passed($start_time); $opts_json = !empty($opts) ? json_encode($opts) : 'null'; $msg = "Stream open failed for '{$url}'; req_time: {$req_time}; opts: {$opts_json}"; $this->log($msg); if ($this->raise_fopen_exception) { throw new Exception($msg); } else { $resp_data['error'] = true; $resp_data['error_msg'] = $msg; } } else { $this->log("Getting metadata…"); $resp_data['meta'] = stream_get_meta_data($stream); $this->log("Getting response…"); $resp_data['body'] = stream_get_contents($stream); $this->log("Closing stream…"); fclose($stream); } if ($this->debug) { $req_time = static::calc_time_passed($start_time); $this->log(sprintf("Request time for \"%s %s\": %f", $opts['http']['method'], $url, $req_time)); } return $resp_data; } /** * If we get back something that claims to be XML or JSON, parse it as such and assign to $resp['body'] * * @param string $resp * @return string|object * @see Resty::$JSON_TYPES * @see Resty::$XML_TYPES */ protected function processResponseBody($resp) { if ($this->parse_body === true) { $header_content_type = isset($resp['headers']['Content-Type']) ? $resp['headers']['Content-Type'] : null; $content_type = preg_split('/[;\s]+/', $header_content_type); $content_type = $content_type[0]; if (in_array($content_type, static::$JSON_TYPES)) { $this->log("Response body is JSON"); $resp['body_raw'] = $resp['body']; $resp['body'] = json_decode($resp['body']); return $resp; } elseif (in_array($content_type, static::$XML_TYPES)) { $this->log("Response body is XML"); $resp['body_raw'] = $resp['body']; $resp['body'] = new \SimpleXMLElement($resp['body']); return $resp; } } $this->log("Response body not parsed"); return $resp; } /** * calculate time passed in microtime * @param float $start_time should be result of microtime(true) * @return float the diff between passed microtime and current microtime */ protected static function calc_time_passed($start_time) { $stop_time = microtime(true); $req_time = $stop_time - $start_time; return $req_time; } protected function log($msg) { if (!$this->debug) { return; } if (is_callable($this->logger)) { $logger = $this->logger; return $logger($msg); } return $this->default_logger($msg); } /** * logging helper * * @param mixed $msg */ protected function default_logger($msg) { $line = date(\DateTime::RFC822) . " :: "; if (is_string($msg)) { $line .= "{$msg}\n"; } else { ob_start(); var_dump($msg); $line = ob_get_clean(); $line .= "\n"; } if (PHP_SAPI !== 'cli') { $line = "
$line
\n"; } return error_log($line); } }