Migrate RainLoop Webmail to use Facebook Graph API instead of FQL

This commit is contained in:
RainLoop Team 2014-07-04 00:11:47 +04:00
parent 9fcaf00875
commit d0dbb26548
120 changed files with 18817 additions and 1746 deletions

View file

@ -6,19 +6,20 @@
function AdminSocial() function AdminSocial()
{ {
var oData = RL.data(); var oData = RL.data();
this.googleEnable = oData.googleEnable; this.googleEnable = oData.googleEnable;
this.googleClientID = oData.googleClientID; this.googleClientID = oData.googleClientID;
this.googleClientSecret = oData.googleClientSecret; this.googleClientSecret = oData.googleClientSecret;
this.googleTrigger1 = ko.observable(Enums.SaveSettingsStep.Idle); this.googleTrigger1 = ko.observable(Enums.SaveSettingsStep.Idle);
this.googleTrigger2 = ko.observable(Enums.SaveSettingsStep.Idle); this.googleTrigger2 = ko.observable(Enums.SaveSettingsStep.Idle);
this.facebookSupported = oData.facebookSupported;
this.facebookEnable = oData.facebookEnable; this.facebookEnable = oData.facebookEnable;
this.facebookAppID = oData.facebookAppID; this.facebookAppID = oData.facebookAppID;
this.facebookAppSecret = oData.facebookAppSecret; this.facebookAppSecret = oData.facebookAppSecret;
this.facebookTrigger1 = ko.observable(Enums.SaveSettingsStep.Idle); this.facebookTrigger1 = ko.observable(Enums.SaveSettingsStep.Idle);
this.facebookTrigger2 = ko.observable(Enums.SaveSettingsStep.Idle); this.facebookTrigger2 = ko.observable(Enums.SaveSettingsStep.Idle);
this.twitterEnable = oData.twitterEnable; this.twitterEnable = oData.twitterEnable;
this.twitterConsumerKey = oData.twitterConsumerKey; this.twitterConsumerKey = oData.twitterConsumerKey;
this.twitterConsumerSecret = oData.twitterConsumerSecret; this.twitterConsumerSecret = oData.twitterConsumerSecret;
@ -48,21 +49,30 @@ AdminSocial.prototype.onBuild = function ()
; ;
self.facebookEnable.subscribe(function (bValue) { self.facebookEnable.subscribe(function (bValue) {
RL.remote().saveAdminConfig(Utils.emptyFunction, { if (self.facebookSupported())
'FacebookEnable': bValue ? '1' : '0' {
}); RL.remote().saveAdminConfig(Utils.emptyFunction, {
'FacebookEnable': bValue ? '1' : '0'
});
}
}); });
self.facebookAppID.subscribe(function (sValue) { self.facebookAppID.subscribe(function (sValue) {
RL.remote().saveAdminConfig(f1, { if (self.facebookSupported())
'FacebookAppID': Utils.trim(sValue) {
}); RL.remote().saveAdminConfig(f1, {
'FacebookAppID': Utils.trim(sValue)
});
}
}); });
self.facebookAppSecret.subscribe(function (sValue) { self.facebookAppSecret.subscribe(function (sValue) {
RL.remote().saveAdminConfig(f2, { if (self.facebookSupported())
'FacebookAppSecret': Utils.trim(sValue) {
}); RL.remote().saveAdminConfig(f2, {
'FacebookAppSecret': Utils.trim(sValue)
});
}
}); });
self.twitterEnable.subscribe(function (bValue) { self.twitterEnable.subscribe(function (bValue) {
@ -82,7 +92,7 @@ AdminSocial.prototype.onBuild = function ()
'TwitterConsumerSecret': Utils.trim(sValue) 'TwitterConsumerSecret': Utils.trim(sValue)
}); });
}); });
self.googleEnable.subscribe(function (bValue) { self.googleEnable.subscribe(function (bValue) {
RL.remote().saveAdminConfig(Utils.emptyFunction, { RL.remote().saveAdminConfig(Utils.emptyFunction, {
'GoogleEnable': bValue ? '1' : '0' 'GoogleEnable': bValue ? '1' : '0'
@ -100,7 +110,7 @@ AdminSocial.prototype.onBuild = function ()
'GoogleClientSecret': Utils.trim(sValue) 'GoogleClientSecret': Utils.trim(sValue)
}); });
}); });
self.dropboxEnable.subscribe(function (bValue) { self.dropboxEnable.subscribe(function (bValue) {
RL.remote().saveAdminConfig(Utils.emptyFunction, { RL.remote().saveAdminConfig(Utils.emptyFunction, {
'DropboxEnable': bValue ? '1' : '0' 'DropboxEnable': bValue ? '1' : '0'

View file

@ -777,7 +777,7 @@ Utils.initDataConstructorBySettings = function (oData)
oData.capaThemes = ko.observable(false); oData.capaThemes = ko.observable(false);
oData.allowLanguagesOnSettings = ko.observable(true); oData.allowLanguagesOnSettings = ko.observable(true);
oData.allowLanguagesOnLogin = ko.observable(true); oData.allowLanguagesOnLogin = ko.observable(true);
oData.useLocalProxyForExternalImages = ko.observable(false); oData.useLocalProxyForExternalImages = ko.observable(false);
oData.desktopNotifications = ko.observable(false); oData.desktopNotifications = ko.observable(false);
@ -968,6 +968,7 @@ Utils.initDataConstructorBySettings = function (oData)
} }
}); });
oData.facebookSupported = ko.observable(false);
oData.facebookEnable = ko.observable(false); oData.facebookEnable = ko.observable(false);
oData.facebookAppID = ko.observable(''); oData.facebookAppID = ko.observable('');
oData.facebookAppSecret = ko.observable(''); oData.facebookAppSecret = ko.observable('');

View file

@ -112,7 +112,7 @@ AbstractData.prototype.populateDataOnStart = function()
{ {
this.layout(mLayout); this.layout(mLayout);
} }
this.facebookSupported(!!RL.settingsGet('SupportedFacebookSocial'));
this.facebookEnable(!!RL.settingsGet('AllowFacebookSocial')); this.facebookEnable(!!RL.settingsGet('AllowFacebookSocial'));
this.facebookAppID(RL.settingsGet('FacebookAppID')); this.facebookAppID(RL.settingsGet('FacebookAppID'));
this.facebookAppSecret(RL.settingsGet('FacebookAppSecret')); this.facebookAppSecret(RL.settingsGet('FacebookAppSecret'));

View file

@ -11,6 +11,14 @@ if (!\defined('RAINLOOP_APP_LIBRARIES_PATH'))
{ {
return include RAINLOOP_APP_LIBRARIES_PATH.'RainLoop/'.\str_replace('\\', '/', \substr($sClassName, 9)).'.php'; return include RAINLOOP_APP_LIBRARIES_PATH.'RainLoop/'.\str_replace('\\', '/', \substr($sClassName, 9)).'.php';
} }
else if (0 === \strpos($sClassName, 'Facebook') && false !== \strpos($sClassName, '\\'))
{
return include RAINLOOP_APP_LIBRARIES_PATH.'Facebook/'.\str_replace('\\', '/', \substr($sClassName, 9)).'.php';
}
else if (0 === \strpos($sClassName, 'GuzzleHttp') && false !== \strpos($sClassName, '\\'))
{
return include RAINLOOP_APP_LIBRARIES_PATH.'GuzzleHttp/'.\str_replace('\\', '/', \substr($sClassName, 11)).'.php';
}
else if (0 === \strpos($sClassName, 'Sabre') && false !== \strpos($sClassName, '\\')) else if (0 === \strpos($sClassName, 'Sabre') && false !== \strpos($sClassName, '\\'))
{ {
if (!RAINLOOP_MB_SUPPORTED && !defined('RL_MB_FIXED')) if (!RAINLOOP_MB_SUPPORTED && !defined('RL_MB_FIXED'))

View file

@ -0,0 +1,370 @@
<?php
/**
* Copyright 2014 Facebook, Inc.
*
* You are hereby granted a non-exclusive, worldwide, royalty-free license to
* use, copy, modify, and distribute this software in source code or binary
* form for use in connection with the web services and APIs provided by
* Facebook.
*
* As with any software that integrates with the Facebook platform, your use
* of this software is subject to the Facebook Developer Principles and
* Policies [http://developers.facebook.com/policy/]. This copyright notice
* shall be included in all copies or substantial portions of the software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
* THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
* DEALINGS IN THE SOFTWARE.
*
*/
namespace Facebook\Entities;
use Facebook\FacebookRequest;
use Facebook\FacebookRequestException;
use Facebook\FacebookSession;
use Facebook\GraphSessionInfo;
/**
* Class AccessToken
* @package Facebook
*/
class AccessToken
{
/**
* The access token.
*
* @var string
*/
protected $accessToken;
/**
* A unique ID to identify a client.
*
* @var string
*/
protected $machineId;
/**
* Date when token expires.
*
* @var \DateTime|null
*/
protected $expiresAt;
/**
* Create a new access token entity.
*
* @param string $accessToken
* @param int $expiresAt
* @param string|null machineId
*/
public function __construct($accessToken, $expiresAt = 0, $machineId = null)
{
$this->accessToken = $accessToken;
if ($expiresAt) {
$this->setExpiresAtFromTimeStamp($expiresAt);
}
$this->machineId = $machineId;
}
/**
* Setter for expires_at.
*
* @param int $timeStamp
*/
protected function setExpiresAtFromTimeStamp($timeStamp)
{
$dt = new \DateTime();
$dt->setTimestamp($timeStamp);
$this->expiresAt = $dt;
}
/**
* Getter for expiresAt.
*
* @return \DateTime|null
*/
public function getExpiresAt()
{
return $this->expiresAt;
}
/**
* Getter for machineId.
*
* @return string|null
*/
public function getMachineId()
{
return $this->machineId;
}
/**
* Determines whether or not this is a long-lived token.
*
* @return bool
*/
public function isLongLived()
{
if ($this->expiresAt) {
return $this->expiresAt->getTimestamp() > time() + (60 * 60 * 2);
}
return false;
}
/**
* Checks the validity of the access token.
*
* @param string|null $appId Application ID to use
* @param string|null $appSecret App secret value to use
* @param string|null $machineId
*
* @return boolean
*/
public function isValid($appId = null, $appSecret = null, $machineId = null)
{
$accessTokenInfo = $this->getInfo($appId, $appSecret);
$machineId = $machineId ?: $this->machineId;
return static::validateAccessToken($accessTokenInfo, $appId, $machineId);
}
/**
* Ensures the provided GraphSessionInfo object is valid,
* throwing an exception if not. Ensures the appId matches,
* that the machineId matches if it's being used,
* that the token is valid and has not expired.
*
* @param GraphSessionInfo $tokenInfo
* @param string|null $appId Application ID to use
* @param string|null $machineId
*
* @return boolean
*/
public static function validateAccessToken(GraphSessionInfo $tokenInfo,
$appId = null, $machineId = null)
{
$targetAppId = FacebookSession::_getTargetAppId($appId);
$appIdIsValid = $tokenInfo->getAppId() == $targetAppId;
$machineIdIsValid = $tokenInfo->getProperty('machine_id') == $machineId;
$accessTokenIsValid = $tokenInfo->isValid();
// Not all access tokens return an expiration. E.g. an app access token.
if ($tokenInfo->getExpiresAt() instanceof \DateTime) {
$accessTokenIsStillAlive = $tokenInfo->getExpiresAt()->getTimestamp() >= time();
} else {
$accessTokenIsStillAlive = true;
}
return $appIdIsValid && $machineIdIsValid && $accessTokenIsValid && $accessTokenIsStillAlive;
}
/**
* Get a valid access token from a code.
*
* @param string $code
* @param string|null $appId
* @param string|null $appSecret
* @param string|null $machineId
*
* @return AccessToken
*/
public static function getAccessTokenFromCode($code, $appId = null, $appSecret = null, $machineId = null)
{
$params = array(
'code' => $code,
'redirect_uri' => '',
);
if ($machineId) {
$params['machine_id'] = $machineId;
}
return static::requestAccessToken($params, $appId, $appSecret);
}
/**
* Get a valid code from an access token.
*
* @param AccessToken|string $accessToken
* @param string|null $appId
* @param string|null $appSecret
*
* @return AccessToken
*/
public static function getCodeFromAccessToken($accessToken, $appId = null, $appSecret = null)
{
$accessToken = (string) $accessToken;
$params = array(
'access_token' => $accessToken,
'redirect_uri' => '',
);
return static::requestCode($params, $appId, $appSecret);
}
/**
* Exchanges a short lived access token with a long lived access token.
*
* @param string|null $appId
* @param string|null $appSecret
*
* @return AccessToken
*/
public function extend($appId = null, $appSecret = null)
{
$params = array(
'grant_type' => 'fb_exchange_token',
'fb_exchange_token' => $this->accessToken,
);
return static::requestAccessToken($params, $appId, $appSecret);
}
/**
* Request an access token based on a set of params.
*
* @param array $params
* @param string|null $appId
* @param string|null $appSecret
*
* @return AccessToken
*
* @throws FacebookRequestException
*/
public static function requestAccessToken(array $params, $appId = null, $appSecret = null)
{
$response = static::request('/oauth/access_token', $params, $appId, $appSecret);
$data = $response->getResponse();
/**
* @TODO fix this malarkey - getResponse() should always return an object
* @see https://github.com/facebook/facebook-php-sdk-v4/issues/36
*/
if (is_array($data)) {
if (isset($data['access_token'])) {
$expiresAt = isset($data['expires']) ? time() + $data['expires'] : 0;
return new static($data['access_token'], $expiresAt);
}
} elseif($data instanceof \stdClass) {
if (isset($data->access_token)) {
$expiresAt = isset($data->expires_in) ? time() + $data->expires_in : 0;
$machineId = isset($data->machine_id) ? (string) $data->machine_id : null;
return new static((string) $data->access_token, $expiresAt, $machineId);
}
}
throw FacebookRequestException::create(
$response->getRawResponse(),
$data,
401
);
}
/**
* Request a code from a long lived access token.
*
* @param array $params
* @param string|null $appId
* @param string|null $appSecret
*
* @return string
*
* @throws FacebookRequestException
*/
public static function requestCode(array $params, $appId = null, $appSecret = null)
{
$response = static::request('/oauth/client_code', $params, $appId, $appSecret);
$data = $response->getResponse();
if (isset($data->code)) {
return (string) $data->code;
}
throw FacebookRequestException::create(
$response->getRawResponse(),
$data,
401
);
}
/**
* Send a request to Graph with an app access token.
*
* @param string $endpoint
* @param array $params
* @param string|null $appId
* @param string|null $appSecret
*
* @return \Facebook\FacebookResponse
*
* @throws FacebookRequestException
*/
protected static function request($endpoint, array $params, $appId = null, $appSecret = null)
{
$targetAppId = FacebookSession::_getTargetAppId($appId);
$targetAppSecret = FacebookSession::_getTargetAppSecret($appSecret);
if (!isset($params['client_id'])) {
$params['client_id'] = $targetAppId;
}
if (!isset($params['client_secret'])) {
$params['client_secret'] = $targetAppSecret;
}
// The response for this endpoint is not JSON, so it must be handled
// differently, not as a GraphObject.
$request = new FacebookRequest(
FacebookSession::newAppSession($targetAppId, $targetAppSecret),
'GET',
$endpoint,
$params
);
return $request->execute();
}
/**
* Get more info about an access token.
*
* @param string|null $appId
* @param string|null $appSecret
*
* @return GraphSessionInfo
*/
public function getInfo($appId = null, $appSecret = null)
{
$params = array('input_token' => $this->accessToken);
$request = new FacebookRequest(
FacebookSession::newAppSession($appId, $appSecret),
'GET',
'/debug_token',
$params
);
$response = $request->execute()->getGraphObject(GraphSessionInfo::className());
// Update the data on this token
if ($response->getExpiresAt()) {
$this->expiresAt = $response->getExpiresAt();
}
return $response;
}
/**
* Returns the access token as a string.
*
* @return string
*/
public function __toString()
{
return $this->accessToken;
}
}

View file

@ -0,0 +1,386 @@
<?php
/**
* Copyright 2014 Facebook, Inc.
*
* You are hereby granted a non-exclusive, worldwide, royalty-free license to
* use, copy, modify, and distribute this software in source code or binary
* form for use in connection with the web services and APIs provided by
* Facebook.
*
* As with any software that integrates with the Facebook platform, your use
* of this software is subject to the Facebook Developer Principles and
* Policies [http://developers.facebook.com/policy/]. This copyright notice
* shall be included in all copies or substantial portions of the software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
* THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
* DEALINGS IN THE SOFTWARE.
*
*/
namespace Facebook\Entities;
use Facebook\FacebookSDKException;
use Facebook\FacebookSession;
/**
* Class SignedRequest
* @package Facebook
*/
class SignedRequest
{
/**
* @var string
*/
public $rawSignedRequest;
/**
* @var array
*/
public $payload;
/**
* Instantiate a new SignedRequest entity.
*
* @param string|null $rawSignedRequest The raw signed request.
* @param string|null $state random string to prevent CSRF.
* @param string|null $appSecret
*/
public function __construct($rawSignedRequest = null, $state = null, $appSecret = null)
{
if (!$rawSignedRequest) {
return;
}
$this->rawSignedRequest = $rawSignedRequest;
$this->payload = static::parse($rawSignedRequest, $state, $appSecret);
}
/**
* Returns the raw signed request data.
*
* @return string|null
*/
public function getRawSignedRequest()
{
return $this->rawSignedRequest;
}
/**
* Returns the parsed signed request data.
*
* @return array|null
*/
public function getPayload()
{
return $this->payload;
}
/**
* Returns a property from the signed request data if available.
*
* @param string $key
* @param mixed|null $default
*
* @return mixed|null
*/
public function get($key, $default = null)
{
if (isset($this->payload[$key])) {
return $this->payload[$key];
}
return $default;
}
/**
* Returns user_id from signed request data if available.
*
* @return string|null
*/
public function getUserId()
{
return $this->get('user_id');
}
/**
* Checks for OAuth data in the payload.
*
* @return boolean
*/
public function hasOAuthData()
{
return isset($this->payload['oauth_token']) || isset($this->payload['code']);
}
/**
* Creates a signed request from an array of data.
*
* @param array $payload
* @param string|null $appSecret
*
* @return string
*/
public static function make(array $payload, $appSecret = null)
{
$payload['algorithm'] = 'HMAC-SHA256';
$payload['issued_at'] = time();
$encodedPayload = static::base64UrlEncode(json_encode($payload));
$hashedSig = static::hashSignature($encodedPayload, $appSecret);
$encodedSig = static::base64UrlEncode($hashedSig);
return $encodedSig.'.'.$encodedPayload;
}
/**
* Validates and decodes a signed request and returns
* the payload as an array.
*
* @param string $signedRequest
* @param string|null $state
* @param string|null $appSecret
*
* @return array
*/
public static function parse($signedRequest, $state = null, $appSecret = null)
{
list($encodedSig, $encodedPayload) = static::split($signedRequest);
// Signature validation
$sig = static::decodeSignature($encodedSig);
$hashedSig = static::hashSignature($encodedPayload, $appSecret);
static::validateSignature($hashedSig, $sig);
// Payload validation
$data = static::decodePayload($encodedPayload);
static::validateAlgorithm($data);
if ($state) {
static::validateCsrf($data, $state);
}
return $data;
}
/**
* Validates the format of a signed request.
*
* @param string $signedRequest
*
* @throws FacebookSDKException
*/
public static function validateFormat($signedRequest)
{
if (strpos($signedRequest, '.') !== false) {
return;
}
throw new FacebookSDKException(
'Malformed signed request.', 606
);
}
/**
* Decodes a raw valid signed request.
*
* @param string $signedRequest
*
* @returns array
*/
public static function split($signedRequest)
{
static::validateFormat($signedRequest);
return explode('.', $signedRequest, 2);
}
/**
* Decodes the raw signature from a signed request.
*
* @param string $encodedSig
*
* @returns string
*
* @throws FacebookSDKException
*/
public static function decodeSignature($encodedSig)
{
$sig = static::base64UrlDecode($encodedSig);
if ($sig) {
return $sig;
}
throw new FacebookSDKException(
'Signed request has malformed encoded signature data.', 607
);
}
/**
* Decodes the raw payload from a signed request.
*
* @param string $encodedPayload
*
* @returns array
*
* @throws FacebookSDKException
*/
public static function decodePayload($encodedPayload)
{
$payload = static::base64UrlDecode($encodedPayload);
if ($payload) {
$payload = json_decode($payload, true);
}
if (is_array($payload)) {
return $payload;
}
throw new FacebookSDKException(
'Signed request has malformed encoded payload data.', 607
);
}
/**
* Validates the algorithm used in a signed request.
*
* @param array $data
*
* @throws FacebookSDKException
*/
public static function validateAlgorithm(array $data)
{
if (isset($data['algorithm']) && $data['algorithm'] === 'HMAC-SHA256') {
return;
}
throw new FacebookSDKException(
'Signed request is using the wrong algorithm.', 605
);
}
/**
* Hashes the signature used in a signed request.
*
* @param string $encodedData
* @param string|null $appSecret
*
* @return string
*
* @throws FacebookSDKException
*/
public static function hashSignature($encodedData, $appSecret = null)
{
$hashedSig = hash_hmac(
'sha256', $encodedData, FacebookSession::_getTargetAppSecret($appSecret), $raw_output = true
);
if ($hashedSig) {
return $hashedSig;
}
throw new FacebookSDKException(
'Unable to hash signature from encoded payload data.', 602
);
}
/**
* Validates the signature used in a signed request.
*
* @param string $hashedSig
* @param string $sig
*
* @throws FacebookSDKException
*/
public static function validateSignature($hashedSig, $sig)
{
if (mb_strlen($hashedSig) === mb_strlen($sig)) {
$validate = 0;
for ($i = 0; $i < mb_strlen($sig); $i++) {
$validate |= ord($hashedSig[$i]) ^ ord($sig[$i]);
}
if ($validate === 0) {
return;
}
}
throw new FacebookSDKException(
'Signed request has an invalid signature.', 602
);
}
/**
* Validates a signed request against CSRF.
*
* @param array $data
* @param string $state
*
* @throws FacebookSDKException
*/
public static function validateCsrf(array $data, $state)
{
if (isset($data['state']) && $data['state'] === $state) {
return;
}
throw new FacebookSDKException(
'Signed request did not pass CSRF validation.', 604
);
}
/**
* Base64 decoding which replaces characters:
* + instead of -
* / instead of _
* @link http://en.wikipedia.org/wiki/Base64#URL_applications
*
* @param string $input base64 url encoded input
*
* @return string decoded string
*/
public static function base64UrlDecode($input)
{
$urlDecodedBase64 = strtr($input, '-_', '+/');
static::validateBase64($urlDecodedBase64);
return base64_decode($urlDecodedBase64);
}
/**
* Base64 encoding which replaces characters:
* + instead of -
* / instead of _
* @link http://en.wikipedia.org/wiki/Base64#URL_applications
*
* @param string $input string to encode
*
* @return string base64 url encoded input
*/
public static function base64UrlEncode($input)
{
return strtr(base64_encode($input), '+/', '-_');
}
/**
* Validates a base64 string.
*
* @param string $input base64 value to validate
*
* @throws FacebookSDKException
*/
public static function validateBase64($input)
{
$pattern = '/^[a-zA-Z0-9\/\r\n+]*={0,2}$/';
if (preg_match($pattern, $input)) {
return;
}
throw new FacebookSDKException(
'Signed request contains malformed base64 encoding.', 608
);
}
}

View file

@ -0,0 +1,33 @@
<?php
/**
* Copyright 2014 Facebook, Inc.
*
* You are hereby granted a non-exclusive, worldwide, royalty-free license to
* use, copy, modify, and distribute this software in source code or binary
* form for use in connection with the web services and APIs provided by
* Facebook.
*
* As with any software that integrates with the Facebook platform, your use
* of this software is subject to the Facebook Developer Principles and
* Policies [http://developers.facebook.com/policy/]. This copyright notice
* shall be included in all copies or substantial portions of the software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
* THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
* DEALINGS IN THE SOFTWARE.
*
*/
namespace Facebook;
/**
* Class FacebookAuthorizationException
* @package Facebook
*/
class FacebookAuthorizationException extends FacebookRequestException
{
}

View file

@ -0,0 +1,72 @@
<?php
/**
* Copyright 2014 Facebook, Inc.
*
* You are hereby granted a non-exclusive, worldwide, royalty-free license to
* use, copy, modify, and distribute this software in source code or binary
* form for use in connection with the web services and APIs provided by
* Facebook.
*
* As with any software that integrates with the Facebook platform, your use
* of this software is subject to the Facebook Developer Principles and
* Policies [http://developers.facebook.com/policy/]. This copyright notice
* shall be included in all copies or substantial portions of the software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
* THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
* DEALINGS IN THE SOFTWARE.
*
*/
namespace Facebook;
/**
* Class FacebookCanvasLoginHelper
* @package Facebook
* @author Fosco Marotto <fjm@fb.com>
* @author David Poll <depoll@fb.com>
*/
class FacebookCanvasLoginHelper extends FacebookSignedRequestFromInputHelper
{
/**
* Returns the app data value.
*
* @return mixed|null
*/
public function getAppData()
{
return $this->signedRequest ? $this->signedRequest->get('app_data') : null;
}
/**
* Get raw signed request from either GET or POST.
*
* @return string|null
*/
public function getRawSignedRequest()
{
/**
* v2.0 apps use GET for Canvas signed requests.
*/
$rawSignedRequest = $this->getRawSignedRequestFromGet();
if ($rawSignedRequest) {
return $rawSignedRequest;
}
/**
* v1.0 apps use POST for Canvas signed requests, will eventually be
* deprecated.
*/
$rawSignedRequest = $this->getRawSignedRequestFromPost();
if ($rawSignedRequest) {
return $rawSignedRequest;
}
return null;
}
}

View file

@ -0,0 +1,33 @@
<?php
/**
* Copyright 2014 Facebook, Inc.
*
* You are hereby granted a non-exclusive, worldwide, royalty-free license to
* use, copy, modify, and distribute this software in source code or binary
* form for use in connection with the web services and APIs provided by
* Facebook.
*
* As with any software that integrates with the Facebook platform, your use
* of this software is subject to the Facebook Developer Principles and
* Policies [http://developers.facebook.com/policy/]. This copyright notice
* shall be included in all copies or substantial portions of the software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
* THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
* DEALINGS IN THE SOFTWARE.
*
*/
namespace Facebook;
/**
* Class FacebookClientException
* @package Facebook
*/
class FacebookClientException extends FacebookRequestException
{
}

View file

@ -0,0 +1,45 @@
<?php
/**
* Copyright 2014 Facebook, Inc.
*
* You are hereby granted a non-exclusive, worldwide, royalty-free license to
* use, copy, modify, and distribute this software in source code or binary
* form for use in connection with the web services and APIs provided by
* Facebook.
*
* As with any software that integrates with the Facebook platform, your use
* of this software is subject to the Facebook Developer Principles and
* Policies [http://developers.facebook.com/policy/]. This copyright notice
* shall be included in all copies or substantial portions of the software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
* THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
* DEALINGS IN THE SOFTWARE.
*
*/
namespace Facebook;
/**
* Class FacebookJavaScriptLoginHelper
* @package Facebook
* @author Fosco Marotto <fjm@fb.com>
* @author David Poll <depoll@fb.com>
*/
class FacebookJavaScriptLoginHelper extends FacebookSignedRequestFromInputHelper
{
/**
* Get raw signed request from the cookie.
*
* @return string|null
*/
public function getRawSignedRequest()
{
return $this->getRawSignedRequestFromCookie();
}
}

View file

@ -0,0 +1,33 @@
<?php
/**
* Copyright 2014 Facebook, Inc.
*
* You are hereby granted a non-exclusive, worldwide, royalty-free license to
* use, copy, modify, and distribute this software in source code or binary
* form for use in connection with the web services and APIs provided by
* Facebook.
*
* As with any software that integrates with the Facebook platform, your use
* of this software is subject to the Facebook Developer Principles and
* Policies [http://developers.facebook.com/policy/]. This copyright notice
* shall be included in all copies or substantial portions of the software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
* THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
* DEALINGS IN THE SOFTWARE.
*
*/
namespace Facebook;
/**
* Class FacebookOtherException
* @package Facebook
*/
class FacebookOtherException extends FacebookRequestException
{
}

View file

@ -0,0 +1,102 @@
<?php
/**
* Copyright 2014 Facebook, Inc.
*
* You are hereby granted a non-exclusive, worldwide, royalty-free license to
* use, copy, modify, and distribute this software in source code or binary
* form for use in connection with the web services and APIs provided by
* Facebook.
*
* As with any software that integrates with the Facebook platform, your use
* of this software is subject to the Facebook Developer Principles and
* Policies [http://developers.facebook.com/policy/]. This copyright notice
* shall be included in all copies or substantial portions of the software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
* THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
* DEALINGS IN THE SOFTWARE.
*
*/
namespace Facebook;
/**
* Class FacebookPageTabHelper
* @package Facebook
* @author Fosco Marotto <fjm@fb.com>
*/
class FacebookPageTabHelper extends FacebookCanvasLoginHelper
{
/**
* @var array|null
*/
protected $pageData;
/**
* Initialize the helper and process available signed request data.
*
* @param string|null $appId
* @param string|null $appSecret
*/
public function __construct($appId = null, $appSecret = null)
{
parent::__construct($appId, $appSecret);
if (!$this->signedRequest) {
return;
}
$this->pageData = $this->signedRequest->get('page');
}
/**
* Returns a value from the page data.
*
* @param string $key
* @param mixed|null $default
*
* @return mixed|null
*/
public function getPageData($key, $default = null)
{
if (isset($this->pageData[$key])) {
return $this->pageData[$key];
}
return $default;
}
/**
* Returns true if the page is liked by the user.
*
* @return boolean
*/
public function isLiked()
{
return $this->getPageData('liked') === true;
}
/**
* Returns true if the user is an admin.
*
* @return boolean
*/
public function isAdmin()
{
return $this->getPageData('admin') === true;
}
/**
* Returns the page id if available.
*
* @return string|null
*/
public function getPageId()
{
return $this->getPageData('id');
}
}

View file

@ -0,0 +1,33 @@
<?php
/**
* Copyright 2014 Facebook, Inc.
*
* You are hereby granted a non-exclusive, worldwide, royalty-free license to
* use, copy, modify, and distribute this software in source code or binary
* form for use in connection with the web services and APIs provided by
* Facebook.
*
* As with any software that integrates with the Facebook platform, your use
* of this software is subject to the Facebook Developer Principles and
* Policies [http://developers.facebook.com/policy/]. This copyright notice
* shall be included in all copies or substantial portions of the software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
* THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
* DEALINGS IN THE SOFTWARE.
*
*/
namespace Facebook;
/**
* Class FacebookPermissionException
* @package Facebook
*/
class FacebookPermissionException extends FacebookRequestException
{
}

View file

@ -0,0 +1,277 @@
<?php
/**
* Copyright 2014 Facebook, Inc.
*
* You are hereby granted a non-exclusive, worldwide, royalty-free license to
* use, copy, modify, and distribute this software in source code or binary
* form for use in connection with the web services and APIs provided by
* Facebook.
*
* As with any software that integrates with the Facebook platform, your use
* of this software is subject to the Facebook Developer Principles and
* Policies [http://developers.facebook.com/policy/]. This copyright notice
* shall be included in all copies or substantial portions of the software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
* THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
* DEALINGS IN THE SOFTWARE.
*
*/
namespace Facebook;
/**
* Class FacebookRedirectLoginHelper
* @package Facebook
* @author Fosco Marotto <fjm@fb.com>
* @author David Poll <depoll@fb.com>
*/
class FacebookRedirectLoginHelper
{
/**
* @var string The application id
*/
private $appId;
/**
* @var string The application secret
*/
private $appSecret;
/**
* @var string The redirect URL for the application
*/
private $redirectUrl;
/**
* @var string Prefix to use for session variables
*/
private $sessionPrefix = 'FBRLH_';
/**
* @var string State token for CSRF validation
*/
protected $state;
/**
* @var boolean Toggle for PHP session status check
*/
protected $checkForSessionStatus = true;
/**
* Constructs a RedirectLoginHelper for a given appId and redirectUrl.
*
* @param string $redirectUrl The URL Facebook should redirect users to
* after login
* @param string $appId The application id
* @param string $appSecret The application secret
*/
public function __construct($redirectUrl, $appId = null, $appSecret = null)
{
$this->appId = FacebookSession::_getTargetAppId($appId);
$this->appSecret = FacebookSession::_getTargetAppSecret($appSecret);
$this->redirectUrl = $redirectUrl;
}
/**
* Stores CSRF state and returns a URL to which the user should be sent to
* in order to continue the login process with Facebook. The
* provided redirectUrl should invoke the handleRedirect method.
*
* @param array $scope List of permissions to request during login
* @param string $version Optional Graph API version if not default (v2.0)
*
* @return string
*/
public function getLoginUrl($scope = array(), $version = null)
{
$version = ($version ?: FacebookRequest::GRAPH_API_VERSION);
$this->state = $this->random(16);
$this->storeState($this->state);
$params = array(
'client_id' => $this->appId,
'redirect_uri' => $this->redirectUrl,
'state' => $this->state,
'sdk' => 'php-sdk-' . FacebookRequest::VERSION,
'scope' => implode(',', $scope)
);
return 'https://www.facebook.com/' . $version . '/dialog/oauth?' .
http_build_query($params, null, '&');
}
/**
* Returns the URL to send the user in order to log out of Facebook.
*
* @param FacebookSession $session The session that will be logged out
* @param string $next The url Facebook should redirect the user to after
* a successful logout
*
* @return string
*/
public function getLogoutUrl(FacebookSession $session, $next)
{
$params = array(
'next' => $next,
'access_token' => $session->getToken()
);
return 'https://www.facebook.com/logout.php?' . http_build_query($params, null, '&');
}
/**
* Handles a response from Facebook, including a CSRF check, and returns a
* FacebookSession.
*
* @return FacebookSession|null
*/
public function getSessionFromRedirect()
{
$this->loadState();
if ($this->isValidRedirect()) {
$params = array(
'client_id' => FacebookSession::_getTargetAppId($this->appId),
'redirect_uri' => $this->redirectUrl,
'client_secret' =>
FacebookSession::_getTargetAppSecret($this->appSecret),
'code' => $this->getCode()
);
$response = (new FacebookRequest(
FacebookSession::newAppSession($this->appId, $this->appSecret),
'GET',
'/oauth/access_token',
$params
))->execute()->getResponse();
if (isset($response['access_token'])) {
return new FacebookSession($response['access_token']);
}
}
return null;
}
/**
* Check if a redirect has a valid state.
*
* @return bool
*/
protected function isValidRedirect()
{
return $this->getCode() && isset($_GET['state'])
&& $_GET['state'] == $this->state;
}
/**
* Return the code.
*
* @return string|null
*/
protected function getCode()
{
return isset($_GET['code']) ? $_GET['code'] : null;
}
/**
* Stores a state string in session storage for CSRF protection.
* Developers should subclass and override this method if they want to store
* this state in a different location.
*
* @param string $state
*
* @throws FacebookSDKException
*/
protected function storeState($state)
{
if ($this->checkForSessionStatus === true
&& session_status() !== PHP_SESSION_ACTIVE) {
throw new FacebookSDKException(
'Session not active, could not store state.', 720
);
}
$_SESSION[$this->sessionPrefix . 'state'] = $state;
}
/**
* Loads a state string from session storage for CSRF validation. May return
* null if no object exists. Developers should subclass and override this
* method if they want to load the state from a different location.
*
* @return string|null
*
* @throws FacebookSDKException
*/
protected function loadState()
{
if ($this->checkForSessionStatus === true
&& session_status() !== PHP_SESSION_ACTIVE) {
throw new FacebookSDKException(
'Session not active, could not load state.', 721
);
}
if (isset($_SESSION[$this->sessionPrefix . 'state'])) {
$this->state = $_SESSION[$this->sessionPrefix . 'state'];
return $this->state;
}
return null;
}
/**
* Generate a cryptographically secure pseudrandom number
*
* @param integer $bytes - number of bytes to return
*
* @return string
*
* @throws FacebookSDKException
*
* @todo Support Windows platforms
*/
public function random($bytes)
{
if (!is_numeric($bytes)) {
throw new FacebookSDKException(
"random() expects an integer"
);
}
if ($bytes < 1) {
throw new FacebookSDKException(
"random() expects an integer greater than zero"
);
}
$buf = '';
// http://sockpuppet.org/blog/2014/02/25/safely-generate-random-numbers/
if (is_readable('/dev/urandom')) {
$fp = fopen('/dev/urandom', 'rb');
if ($fp !== FALSE) {
$buf = fread($fp, $bytes);
fclose($fp);
if($buf !== FALSE) {
return bin2hex($buf);
}
}
}
if (function_exists('mcrypt_create_iv')) {
$buf = mcrypt_create_iv($bytes, MCRYPT_DEV_URANDOM);
if ($buf !== FALSE) {
return bin2hex($buf);
}
}
while (strlen($buf) < $bytes) {
$buf .= md5(uniqid(mt_rand(), true), true);
// We are appending raw binary
}
return bin2hex(substr($buf, 0, $bytes));
}
/**
* Disables the session_status() check when using $_SESSION
*/
public function disableSessionStatusCheck()
{
$this->checkForSessionStatus = false;
}
}

View file

@ -0,0 +1,313 @@
<?php
/**
* Copyright 2014 Facebook, Inc.
*
* You are hereby granted a non-exclusive, worldwide, royalty-free license to
* use, copy, modify, and distribute this software in source code or binary
* form for use in connection with the web services and APIs provided by
* Facebook.
*
* As with any software that integrates with the Facebook platform, your use
* of this software is subject to the Facebook Developer Principles and
* Policies [http://developers.facebook.com/policy/]. This copyright notice
* shall be included in all copies or substantial portions of the software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
* THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
* DEALINGS IN THE SOFTWARE.
*
*/
namespace Facebook;
use Facebook\HttpClients\FacebookHttpable;
use Facebook\HttpClients\FacebookCurlHttpClient;
use Facebook\HttpClients\FacebookStreamHttpClient;
/**
* Class FacebookRequest
* @package Facebook
* @author Fosco Marotto <fjm@fb.com>
* @author David Poll <depoll@fb.com>
*/
class FacebookRequest
{
/**
* @const string Version number of the Facebook PHP SDK.
*/
const VERSION = '4.0.9';
/**
* @const string Default Graph API version for requests
*/
const GRAPH_API_VERSION = 'v2.0';
/**
* @const string Graph API URL
*/
const BASE_GRAPH_URL = 'https://graph.facebook.com';
/**
* @var FacebookSession The session used for this request
*/
private $session;
/**
* @var string The HTTP method for the request
*/
private $method;
/**
* @var string The path for the request
*/
private $path;
/**
* @var array The parameters for the request
*/
private $params;
/**
* @var string The Graph API version for the request
*/
private $version;
/**
* @var string ETag sent with the request
*/
private $etag;
/**
* @var FacebookHttpable HTTP client handler
*/
private static $httpClientHandler;
/**
* @var int The number of calls that have been made to Graph.
*/
public static $requestCount = 0;
/**
* getSession - Returns the associated FacebookSession.
*
* @return FacebookSession
*/
public function getSession()
{
return $this->session;
}
/**
* getPath - Returns the associated path.
*
* @return string
*/
public function getPath()
{
return $this->path;
}
/**
* getParameters - Returns the associated parameters.
*
* @return array
*/
public function getParameters()
{
return $this->params;
}
/**
* getMethod - Returns the associated method.
*
* @return string
*/
public function getMethod()
{
return $this->method;
}
/**
* getETag - Returns the ETag sent with the request.
*
* @return string
*/
public function getETag()
{
return $this->etag;
}
/**
* setHttpClientHandler - Returns an instance of the HTTP client
* handler
*
* @param \Facebook\HttpClients\FacebookHttpable
*/
public static function setHttpClientHandler(FacebookHttpable $handler)
{
static::$httpClientHandler = $handler;
}
/**
* getHttpClientHandler - Returns an instance of the HTTP client
* data handler
*
* @return FacebookHttpable
*/
public static function getHttpClientHandler()
{
if (static::$httpClientHandler) {
return static::$httpClientHandler;
}
return function_exists('curl_init') ? new FacebookCurlHttpClient() : new FacebookStreamHttpClient();
}
/**
* FacebookRequest - Returns a new request using the given session. optional
* parameters hash will be sent with the request. This object is
* immutable.
*
* @param FacebookSession $session
* @param string $method
* @param string $path
* @param array|null $parameters
* @param string|null $version
* @param string|null $etag
*/
public function __construct(
FacebookSession $session, $method, $path, $parameters = null, $version = null, $etag = null
)
{
$this->session = $session;
$this->method = $method;
$this->path = $path;
if ($version) {
$this->version = $version;
} else {
$this->version = static::GRAPH_API_VERSION;
}
$this->etag = $etag;
$params = ($parameters ?: array());
if ($session
&& !isset($params["access_token"])) {
$params["access_token"] = $session->getToken();
}
if (FacebookSession::useAppSecretProof()
&& !isset($params["appsecret_proof"])) {
$params["appsecret_proof"] = $this->getAppSecretProof(
$params["access_token"]
);
}
$this->params = $params;
}
/**
* Returns the base Graph URL.
*
* @return string
*/
protected function getRequestURL()
{
return static::BASE_GRAPH_URL . '/' . $this->version . $this->path;
}
/**
* execute - Makes the request to Facebook and returns the result.
*
* @return FacebookResponse
*
* @throws FacebookSDKException
* @throws FacebookRequestException
*/
public function execute()
{
$url = $this->getRequestURL();
$params = $this->getParameters();
if ($this->method === "GET") {
$url = self::appendParamsToUrl($url, $params);
$params = array();
}
$connection = self::getHttpClientHandler();
$connection->addRequestHeader('User-Agent', 'fb-php-' . self::VERSION);
$connection->addRequestHeader('Accept-Encoding', '*'); // Support all available encodings.
// ETag
if (isset($this->etag)) {
$connection->addRequestHeader('If-None-Match', $this->etag);
}
// Should throw `FacebookSDKException` exception on HTTP client error.
// Don't catch to allow it to bubble up.
$result = $connection->send($url, $this->method, $params);
static::$requestCount++;
$etagHit = 304 == $connection->getResponseHttpStatusCode();
$headers = $connection->getResponseHeaders();
$etagReceived = isset($headers['ETag']) ? $headers['ETag'] : null;
$decodedResult = json_decode($result);
if ($decodedResult === null) {
$out = array();
parse_str($result, $out);
return new FacebookResponse($this, $out, $result, $etagHit, $etagReceived);
}
if (isset($decodedResult->error)) {
throw FacebookRequestException::create(
$result,
$decodedResult->error,
$connection->getResponseHttpStatusCode()
);
}
return new FacebookResponse($this, $decodedResult, $result, $etagHit, $etagReceived);
}
/**
* Generate and return the appsecret_proof value for an access_token
*
* @param string $token
*
* @return string
*/
public function getAppSecretProof($token)
{
return hash_hmac('sha256', $token, FacebookSession::_getTargetAppSecret());
}
/**
* appendParamsToUrl - Gracefully appends params to the URL.
*
* @param string $url
* @param array $params
*
* @return string
*/
public static function appendParamsToUrl($url, $params = array())
{
if (!$params) {
return $url;
}
if (strpos($url, '?') === false) {
return $url . '?' . http_build_query($params, null, '&');
}
list($path, $query_string) = explode('?', $url, 2);
parse_str($query_string, $query_array);
// Favor params from the original URL over $params
$params = array_merge($params, $query_array);
return $path . '?' . http_build_query($params, null, '&');
}
}

View file

@ -0,0 +1,222 @@
<?php
/**
* Copyright 2014 Facebook, Inc.
*
* You are hereby granted a non-exclusive, worldwide, royalty-free license to
* use, copy, modify, and distribute this software in source code or binary
* form for use in connection with the web services and APIs provided by
* Facebook.
*
* As with any software that integrates with the Facebook platform, your use
* of this software is subject to the Facebook Developer Principles and
* Policies [http://developers.facebook.com/policy/]. This copyright notice
* shall be included in all copies or substantial portions of the software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
* THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
* DEALINGS IN THE SOFTWARE.
*
*/
namespace Facebook;
/**
* Class FacebookRequestException
* @package Facebook
* @author Fosco Marotto <fjm@fb.com>
* @author David Poll <depoll@fb.com>
*/
class FacebookRequestException extends FacebookSDKException
{
/**
* @var int Status code for the response causing the exception
*/
private $statusCode;
/**
* @var string Raw response
*/
private $rawResponse;
/**
* @var array Decoded response
*/
private $responseData;
/**
* Creates a FacebookRequestException.
*
* @param string $rawResponse The raw response from the Graph API
* @param array $responseData The decoded response from the Graph API
* @param int $statusCode
*/
public function __construct($rawResponse, $responseData, $statusCode)
{
$this->rawResponse = $rawResponse;
$this->statusCode = $statusCode;
$this->responseData = self::convertToArray($responseData);
parent::__construct(
$this->get('message', 'Unknown Exception'), $this->get('code', -1), null
);
}
/**
* Process an error payload from the Graph API and return the appropriate
* exception subclass.
*
* @param string $raw the raw response from the Graph API
* @param array $data the decoded response from the Graph API
* @param int $statusCode the HTTP response code
*
* @return FacebookRequestException
*/
public static function create($raw, $data, $statusCode)
{
$data = self::convertToArray($data);
if (!isset($data['error']['code']) && isset($data['code'])) {
$data = array('error' => $data);
}
$code = (isset($data['error']['code']) ? $data['error']['code'] : null);
if (isset($data['error']['error_subcode'])) {
switch ($data['error']['error_subcode']) {
// Other authentication issues
case 458:
case 459:
case 460:
case 463:
case 464:
case 467:
return new FacebookAuthorizationException($raw, $data, $statusCode);
break;
}
}
switch ($code) {
// Login status or token expired, revoked, or invalid
case 100:
case 102:
case 190:
return new FacebookAuthorizationException($raw, $data, $statusCode);
break;
// Server issue, possible downtime
case 1:
case 2:
return new FacebookServerException($raw, $data, $statusCode);
break;
// API Throttling
case 4:
case 17:
case 341:
return new FacebookThrottleException($raw, $data, $statusCode);
break;
// Duplicate Post
case 506:
return new FacebookClientException($raw, $data, $statusCode);
break;
}
// Missing Permissions
if ($code == 10 || ($code >= 200 && $code <= 299)) {
return new FacebookPermissionException($raw, $data, $statusCode);
}
// OAuth authentication error
if (isset($data['error']['type'])
and $data['error']['type'] === 'OAuthException') {
return new FacebookAuthorizationException($raw, $data, $statusCode);
}
// All others
return new FacebookOtherException($raw, $data, $statusCode);
}
/**
* Checks isset and returns that or a default value.
*
* @param string $key
* @param mixed $default
*
* @return mixed
*/
private function get($key, $default = null)
{
if (isset($this->responseData['error'][$key])) {
return $this->responseData['error'][$key];
}
return $default;
}
/**
* Returns the HTTP status code
*
* @return int
*/
public function getHttpStatusCode()
{
return $this->statusCode;
}
/**
* Returns the sub-error code
*
* @return int
*/
public function getSubErrorCode()
{
return $this->get('error_subcode', -1);
}
/**
* Returns the error type
*
* @return string
*/
public function getErrorType()
{
return $this->get('type', '');
}
/**
* Returns the raw response used to create the exception.
*
* @return string
*/
public function getRawResponse()
{
return $this->rawResponse;
}
/**
* Returns the decoded response used to create the exception.
*
* @return array
*/
public function getResponse()
{
return $this->responseData;
}
/**
* Converts a stdClass object to an array
*
* @param mixed $object
*
* @return array
*/
private static function convertToArray($object)
{
if ($object instanceof \stdClass) {
return get_object_vars($object);
}
return $object;
}
}

View file

@ -0,0 +1,202 @@
<?php
/**
* Copyright 2014 Facebook, Inc.
*
* You are hereby granted a non-exclusive, worldwide, royalty-free license to
* use, copy, modify, and distribute this software in source code or binary
* form for use in connection with the web services and APIs provided by
* Facebook.
*
* As with any software that integrates with the Facebook platform, your use
* of this software is subject to the Facebook Developer Principles and
* Policies [http://developers.facebook.com/policy/]. This copyright notice
* shall be included in all copies or substantial portions of the software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
* THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
* DEALINGS IN THE SOFTWARE.
*
*/
namespace Facebook;
/**
* Class FacebookResponse
* @package Facebook
* @author Fosco Marotto <fjm@fb.com>
* @author David Poll <depoll@fb.com>
*/
class FacebookResponse
{
/**
* @var FacebookRequest The request which produced this response
*/
private $request;
/**
* @var array The decoded response from the Graph API
*/
private $responseData;
/**
* @var string The raw response from the Graph API
*/
private $rawResponse;
/**
* @var bool Indicates whether sent ETag matched the one on the FB side
*/
private $etagHit;
/**
* @var string ETag received with the response. `null` in case of ETag hit.
*/
private $etag;
/**
* Creates a FacebookResponse object for a given request and response.
*
* @param FacebookRequest $request
* @param array $responseData JSON Decoded response data
* @param string $rawResponse Raw string response
* @param bool $etagHit Indicates whether sent ETag matched the one on the FB side
* @param string|null $etag ETag received with the response. `null` in case of ETag hit.
*/
public function __construct($request, $responseData, $rawResponse, $etagHit = false, $etag = null)
{
$this->request = $request;
$this->responseData = $responseData;
$this->rawResponse = $rawResponse;
$this->etagHit = $etagHit;
$this->etag = $etag;
}
/**
* Returns the request which produced this response.
*
* @return FacebookRequest
*/
public function getRequest()
{
return $this->request;
}
/**
* Returns the decoded response data.
*
* @return array
*/
public function getResponse()
{
return $this->responseData;
}
/**
* Returns the raw response
*
* @return string
*/
public function getRawResponse()
{
return $this->rawResponse;
}
/**
* Returns true if ETag matched the one sent with a request
*
* @return bool
*/
public function isETagHit()
{
return $this->etagHit;
}
/**
* Returns the ETag
*
* @return string
*/
public function getETag()
{
return $this->etag;
}
/**
* Gets the result as a GraphObject. If a type is specified, returns the
* strongly-typed subclass of GraphObject for the data.
*
* @param string $type
*
* @return mixed
*/
public function getGraphObject($type = 'Facebook\GraphObject') {
return (new GraphObject($this->responseData))->cast($type);
}
/**
* Returns an array of GraphObject returned by the request. If a type is
* specified, returns the strongly-typed subclass of GraphObject for the data.
*
* @param string $type
*
* @return mixed
*/
public function getGraphObjectList($type = 'Facebook\GraphObject') {
$out = array();
$data = $this->responseData->data;
for ($i = 0; $i < count($data); $i++) {
$out[] = (new GraphObject($data[$i]))->cast($type);
}
return $out;
}
/**
* If this response has paginated data, returns the FacebookRequest for the
* next page, or null.
*
* @return FacebookRequest|null
*/
public function getRequestForNextPage()
{
return $this->handlePagination('next');
}
/**
* If this response has paginated data, returns the FacebookRequest for the
* previous page, or null.
*
* @return FacebookRequest|null
*/
public function getRequestForPreviousPage()
{
return $this->handlePagination('previous');
}
/**
* Returns the FacebookRequest for the previous or next page, or null.
*
* @param string $direction
*
* @return FacebookRequest|null
*/
private function handlePagination($direction) {
if (isset($this->responseData->paging->$direction)) {
$url = parse_url($this->responseData->paging->$direction);
parse_str($url['query'], $params);
return new FacebookRequest(
$this->request->getSession(),
$this->request->getMethod(),
$this->request->getPath(),
$params
);
} else {
return null;
}
}
}

View file

@ -0,0 +1,33 @@
<?php
/**
* Copyright 2014 Facebook, Inc.
*
* You are hereby granted a non-exclusive, worldwide, royalty-free license to
* use, copy, modify, and distribute this software in source code or binary
* form for use in connection with the web services and APIs provided by
* Facebook.
*
* As with any software that integrates with the Facebook platform, your use
* of this software is subject to the Facebook Developer Principles and
* Policies [http://developers.facebook.com/policy/]. This copyright notice
* shall be included in all copies or substantial portions of the software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
* THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
* DEALINGS IN THE SOFTWARE.
*
*/
namespace Facebook;
/**
* Class FacebookSDKException
* @package Facebook
*/
class FacebookSDKException extends \Exception
{
}

View file

@ -0,0 +1,33 @@
<?php
/**
* Copyright 2014 Facebook, Inc.
*
* You are hereby granted a non-exclusive, worldwide, royalty-free license to
* use, copy, modify, and distribute this software in source code or binary
* form for use in connection with the web services and APIs provided by
* Facebook.
*
* As with any software that integrates with the Facebook platform, your use
* of this software is subject to the Facebook Developer Principles and
* Policies [http://developers.facebook.com/policy/]. This copyright notice
* shall be included in all copies or substantial portions of the software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
* THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
* DEALINGS IN THE SOFTWARE.
*
*/
namespace Facebook;
/**
* Class FacebookServerException
* @package Facebook
*/
class FacebookServerException extends FacebookRequestException
{
}

View file

@ -0,0 +1,367 @@
<?php
/**
* Copyright 2014 Facebook, Inc.
*
* You are hereby granted a non-exclusive, worldwide, royalty-free license to
* use, copy, modify, and distribute this software in source code or binary
* form for use in connection with the web services and APIs provided by
* Facebook.
*
* As with any software that integrates with the Facebook platform, your use
* of this software is subject to the Facebook Developer Principles and
* Policies [http://developers.facebook.com/policy/]. This copyright notice
* shall be included in all copies or substantial portions of the software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
* THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
* DEALINGS IN THE SOFTWARE.
*
*/
namespace Facebook;
use Facebook\Entities\AccessToken;
use Facebook\Entities\SignedRequest;
/**
* Class FacebookSession
* @package Facebook
* @author Fosco Marotto <fjm@fb.com>
* @author David Poll <depoll@fb.com>
*/
class FacebookSession
{
/**
* @var string
*/
private static $defaultAppId;
/**
* @var string
*/
private static $defaultAppSecret;
/**
* @var AccessToken The AccessToken entity for this connection.
*/
private $accessToken;
/**
* @var SignedRequest
*/
private $signedRequest;
/**
* @var bool
*/
private static $useAppSecretProof = true;
/**
* When creating a Session from an access_token, use:
* var $session = new FacebookSession($accessToken);
* This will validate the token and provide a Session object ready for use.
* It will throw a SessionException in case of error.
*
* @param AccessToken|string $accessToken
* @param SignedRequest $signedRequest The SignedRequest entity
*/
public function __construct($accessToken, SignedRequest $signedRequest = null)
{
$this->accessToken = $accessToken instanceof AccessToken ? $accessToken : new AccessToken($accessToken);
$this->signedRequest = $signedRequest;
}
/**
* Returns the access token.
*
* @return string
*/
public function getToken()
{
return (string) $this->accessToken;
}
/**
* Returns the access token entity.
*
* @return AccessToken
*/
public function getAccessToken()
{
return $this->accessToken;
}
/**
* Returns the SignedRequest entity.
*
* @return SignedRequest
*/
public function getSignedRequest()
{
return $this->signedRequest;
}
/**
* Returns the signed request payload.
*
* @return null|array
*/
public function getSignedRequestData()
{
return $this->signedRequest ? $this->signedRequest->getPayload() : null;
}
/**
* Returns a property from the signed request data if available.
*
* @param string $key
*
* @return null|mixed
*/
public function getSignedRequestProperty($key)
{
return $this->signedRequest ? $this->signedRequest->get($key) : null;
}
/**
* Returns user_id from signed request data if available.
*
* @return null|string
*/
public function getUserId()
{
return $this->signedRequest ? $this->signedRequest->getUserId() : null;
}
// @TODO Remove getSessionInfo() in 4.1: can be accessed from AccessToken directly
/**
* getSessionInfo - Makes a request to /debug_token with the appropriate
* arguments to get debug information about the sessions token.
*
* @param string|null $appId
* @param string|null $appSecret
*
* @return GraphSessionInfo
*/
public function getSessionInfo($appId = null, $appSecret = null)
{
return $this->accessToken->getInfo($appId, $appSecret);
}
// @TODO Remove getLongLivedSession() in 4.1: can be accessed from AccessToken directly
/**
* getLongLivedSession - Returns a new Facebook session resulting from
* extending a short-lived access token. If this session is not
* short-lived, returns $this.
*
* @param string|null $appId
* @param string|null $appSecret
*
* @return FacebookSession
*/
public function getLongLivedSession($appId = null, $appSecret = null)
{
$longLivedAccessToken = $this->accessToken->extend($appId, $appSecret);
return new static($longLivedAccessToken);
}
// @TODO Remove getExchangeToken() in 4.1: can be accessed from AccessToken directly
/**
* getExchangeToken - Returns an exchange token string which can be sent
* back to clients and exchanged for a device-linked access token.
*
* @param string|null $appId
* @param string|null $appSecret
*
* @return string
*/
public function getExchangeToken($appId = null, $appSecret = null)
{
return AccessToken::getCodeFromAccessToken($this->accessToken, $appId, $appSecret);
}
// @TODO Remove validate() in 4.1: can be accessed from AccessToken directly
/**
* validate - Ensures the current session is valid, throwing an exception if
* not. Fetches token info from Facebook.
*
* @param string|null $appId Application ID to use
* @param string|null $appSecret App secret value to use
* @param string|null $machineId
*
* @return boolean
*
* @throws FacebookSDKException
*/
public function validate($appId = null, $appSecret = null, $machineId = null)
{
if ($this->accessToken->isValid($appId, $appSecret, $machineId)) {
return true;
}
// @TODO For v4.1 this should not throw an exception, but just return false.
throw new FacebookSDKException(
'Session has expired, or is not valid for this app.', 601
);
}
// @TODO Remove validateSessionInfo() in 4.1: can be accessed from AccessToken directly
/**
* validateTokenInfo - Ensures the provided GraphSessionInfo object is valid,
* throwing an exception if not. Ensures the appId matches,
* that the token is valid and has not expired.
*
* @param GraphSessionInfo $tokenInfo
* @param string|null $appId Application ID to use
* @param string|null $machineId
*
* @return boolean
*
* @throws FacebookSDKException
*/
public static function validateSessionInfo(GraphSessionInfo $tokenInfo,
$appId = null,
$machineId = null)
{
if (AccessToken::validateAccessToken($tokenInfo, $appId, $machineId)) {
return true;
}
// @TODO For v4.1 this should not throw an exception, but just return false.
throw new FacebookSDKException(
'Session has expired, or is not valid for this app.', 601
);
}
/**
* newSessionFromSignedRequest - Returns a FacebookSession for a
* given signed request.
*
* @param SignedRequest $signedRequest
*
* @return FacebookSession
*/
public static function newSessionFromSignedRequest(SignedRequest $signedRequest)
{
if ($signedRequest->get('code')
&& !$signedRequest->get('oauth_token')) {
return self::newSessionAfterValidation($signedRequest);
}
$accessToken = $signedRequest->get('oauth_token');
$expiresAt = $signedRequest->get('expires', 0);
$accessToken = new AccessToken($accessToken, $expiresAt);
return new static($accessToken, $signedRequest);
}
/**
* newSessionAfterValidation - Returns a FacebookSession for a
* validated & parsed signed request.
*
* @param SignedRequest $signedRequest
*
* @return FacebookSession
*/
protected static function newSessionAfterValidation(SignedRequest $signedRequest)
{
$code = $signedRequest->get('code');
$accessToken = AccessToken::getAccessTokenFromCode($code);
return new static($accessToken, $signedRequest);
}
/**
* newAppSession - Returns a FacebookSession configured with a token for the
* application which can be used for publishing and requesting app-level
* information.
*
* @param string|null $appId Application ID to use
* @param string|null $appSecret App secret value to use
*
* @return FacebookSession
*/
public static function newAppSession($appId = null, $appSecret = null)
{
$targetAppId = static::_getTargetAppId($appId);
$targetAppSecret = static::_getTargetAppSecret($appSecret);
return new FacebookSession(
$targetAppId . '|' . $targetAppSecret
);
}
/**
* setDefaultApplication - Will set the static default appId and appSecret
* to be used for API requests.
*
* @param string $appId Application ID to use by default
* @param string $appSecret App secret value to use by default
*/
public static function setDefaultApplication($appId, $appSecret)
{
self::$defaultAppId = $appId;
self::$defaultAppSecret = $appSecret;
}
/**
* _getTargetAppId - Will return either the provided app Id or the default,
* throwing if neither are populated.
*
* @param string $appId
*
* @return string
*
* @throws FacebookSDKException
*/
public static function _getTargetAppId($appId = null) {
$target = ($appId ?: self::$defaultAppId);
if (!$target) {
throw new FacebookSDKException(
'You must provide or set a default application id.', 700
);
}
return $target;
}
/**
* _getTargetAppSecret - Will return either the provided app secret or the
* default, throwing if neither are populated.
*
* @param string $appSecret
*
* @return string
*
* @throws FacebookSDKException
*/
public static function _getTargetAppSecret($appSecret = null) {
$target = ($appSecret ?: self::$defaultAppSecret);
if (!$target) {
throw new FacebookSDKException(
'You must provide or set a default application secret.', 701
);
}
return $target;
}
/**
* Enable or disable sending the appsecret_proof with requests.
*
* @param bool $on
*/
public static function enableAppSecretProof($on = true)
{
static::$useAppSecretProof = ($on ? true : false);
}
/**
* Get whether or not appsecret_proof should be sent with requests.
*
* @return bool
*/
public static function useAppSecretProof()
{
return static::$useAppSecretProof;
}
}

View file

@ -0,0 +1,166 @@
<?php
/**
* Copyright 2014 Facebook, Inc.
*
* You are hereby granted a non-exclusive, worldwide, royalty-free license to
* use, copy, modify, and distribute this software in source code or binary
* form for use in connection with the web services and APIs provided by
* Facebook.
*
* As with any software that integrates with the Facebook platform, your use
* of this software is subject to the Facebook Developer Principles and
* Policies [http://developers.facebook.com/policy/]. This copyright notice
* shall be included in all copies or substantial portions of the software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
* THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
* DEALINGS IN THE SOFTWARE.
*
*/
namespace Facebook;
use Facebook\Entities\SignedRequest;
/**
* Class FacebookSignedRequestFromInputHelper
* @package Facebook
*/
abstract class FacebookSignedRequestFromInputHelper
{
/**
* @var \Facebook\Entities\SignedRequest|null
*/
protected $signedRequest;
/**
* @var string the app id
*/
protected $appId;
/**
* @var string the app secret
*/
protected $appSecret;
/**
* @var string|null Random string to prevent CSRF.
*/
public $state = null;
/**
* Initialize the helper and process available signed request data.
*
* @param string|null $appId
* @param string|null $appSecret
*/
public function __construct($appId = null, $appSecret = null)
{
$this->appId = FacebookSession::_getTargetAppId($appId);
$this->appSecret = FacebookSession::_getTargetAppSecret($appSecret);
$this->instantiateSignedRequest();
}
/**
* Instantiates a new SignedRequest entity.
*
* @param string|null
*/
public function instantiateSignedRequest($rawSignedRequest = null)
{
$rawSignedRequest = $rawSignedRequest ?: $this->getRawSignedRequest();
if (!$rawSignedRequest) {
return;
}
$this->signedRequest = new SignedRequest($rawSignedRequest, $this->state, $this->appSecret);
}
/**
* Instantiates a FacebookSession from the signed request from input.
*
* @return FacebookSession|null
*/
public function getSession()
{
if ($this->signedRequest && $this->signedRequest->hasOAuthData()) {
return FacebookSession::newSessionFromSignedRequest($this->signedRequest);
}
return null;
}
/**
* Returns the SignedRequest entity.
*
* @return \Facebook\Entities\SignedRequest|null
*/
public function getSignedRequest()
{
return $this->signedRequest;
}
/**
* Returns the user_id if available.
*
* @return string|null
*/
public function getUserId()
{
return $this->signedRequest ? $this->signedRequest->getUserId() : null;
}
/**
* Get raw signed request from input.
*
* @return string|null
*/
abstract public function getRawSignedRequest();
/**
* Get raw signed request from GET input.
*
* @return string|null
*/
public function getRawSignedRequestFromGet()
{
if (isset($_GET['signed_request'])) {
return $_GET['signed_request'];
}
return null;
}
/**
* Get raw signed request from POST input.
*
* @return string|null
*/
public function getRawSignedRequestFromPost()
{
if (isset($_POST['signed_request'])) {
return $_POST['signed_request'];
}
return null;
}
/**
* Get raw signed request from cookie set from the Javascript SDK.
*
* @return string|null
*/
public function getRawSignedRequestFromCookie()
{
if (isset($_COOKIE['fbsr_' . $this->appId])) {
return $_COOKIE['fbsr_' . $this->appId];
}
return null;
}
}

View file

@ -0,0 +1,33 @@
<?php
/**
* Copyright 2014 Facebook, Inc.
*
* You are hereby granted a non-exclusive, worldwide, royalty-free license to
* use, copy, modify, and distribute this software in source code or binary
* form for use in connection with the web services and APIs provided by
* Facebook.
*
* As with any software that integrates with the Facebook platform, your use
* of this software is subject to the Facebook Developer Principles and
* Policies [http://developers.facebook.com/policy/]. This copyright notice
* shall be included in all copies or substantial portions of the software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
* THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
* DEALINGS IN THE SOFTWARE.
*
*/
namespace Facebook;
/**
* Class FacebookThrottleException
* @package Facebook
*/
class FacebookThrottleException extends FacebookRequestException
{
}

View file

@ -0,0 +1,173 @@
<?php
/**
* Copyright 2014 Facebook, Inc.
*
* You are hereby granted a non-exclusive, worldwide, royalty-free license to
* use, copy, modify, and distribute this software in source code or binary
* form for use in connection with the web services and APIs provided by
* Facebook.
*
* As with any software that integrates with the Facebook platform, your use
* of this software is subject to the Facebook Developer Principles and
* Policies [http://developers.facebook.com/policy/]. This copyright notice
* shall be included in all copies or substantial portions of the software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
* THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
* DEALINGS IN THE SOFTWARE.
*
*/
namespace Facebook;
/**
* Class GraphAlbum
* @package Facebook
* @author Daniele Grosso <daniele.grosso@gmail.com>
*/
class GraphAlbum extends GraphObject
{
/**
* Returns the ID for the album.
*
* @return string|null
*/
public function getId()
{
return $this->getProperty('id');
}
/**
* Returns whether the viewer can upload photos to this album.
*
* @return boolean|null
*/
public function canUpload()
{
return $this->getProperty('can_upload');
}
/**
* Returns the number of photos in this album.
*
* @return int|null
*/
public function getCount()
{
return$this->getProperty('count');
}
/**
* Returns the ID of the album's cover photo.
*
* @return string|null
*/
public function getCoverPhoto()
{
return$this->getProperty('cover_photo');
}
/**
* Returns the time the album was initially created.
*
* @return \DateTime|null
*/
public function getCreatedTime()
{
$value = $this->getProperty('created_time');
if ($value) {
return new \DateTime($value);
}
return null;
}
/**
* Returns the time the album was updated.
*
* @return \DateTime|null
*/
public function getUpdatedTime()
{
$value = $this->getProperty('updated_time');
if ($value) {
return new \DateTime($value);
}
return null;
}
/**
* Returns the description of the album.
*
* @return string|null
*/
public function getDescription()
{
return$this->getProperty('description');
}
/**
* Returns profile that created the album.
*
* @return GraphUser|null
*/
public function getFrom()
{
return $this->getProperty('from', GraphUser::className());
}
/**
* Returns a link to this album on Facebook.
*
* @return string|null
*/
public function getLink()
{
return$this->getProperty('link');
}
/**
* Returns the textual location of the album.
*
* @return string|null
*/
public function getLocation()
{
return$this->getProperty('location');
}
/**
* Returns the title of the album.
*
* @return string|null
*/
public function getName()
{
return$this->getProperty('name');
}
/**
* Returns the privacy settings for the album.
*
* @return string|null
*/
public function getPrivacy()
{
return$this->getProperty('privacy');
}
/**
* Returns the type of the album. enum{profile, mobile, wall, normal, album}
*
* @return string|null
*/
public function getType()
{
return$this->getProperty('type');
}
//TODO: public function getPlace() that should return GraphPage
}

View file

@ -0,0 +1,105 @@
<?php
/**
* Copyright 2014 Facebook, Inc.
*
* You are hereby granted a non-exclusive, worldwide, royalty-free license to
* use, copy, modify, and distribute this software in source code or binary
* form for use in connection with the web services and APIs provided by
* Facebook.
*
* As with any software that integrates with the Facebook platform, your use
* of this software is subject to the Facebook Developer Principles and
* Policies [http://developers.facebook.com/policy/]. This copyright notice
* shall be included in all copies or substantial portions of the software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
* THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
* DEALINGS IN THE SOFTWARE.
*
*/
namespace Facebook;
/**
* Class GraphLocation
* @package Facebook
* @author Fosco Marotto <fjm@fb.com>
* @author David Poll <depoll@fb.com>
*/
class GraphLocation extends GraphObject
{
/**
* Returns the street component of the location
*
* @return string|null
*/
public function getStreet()
{
return $this->getProperty('street');
}
/**
* Returns the city component of the location
*
* @return string|null
*/
public function getCity()
{
return $this->getProperty('city');
}
/**
* Returns the state component of the location
*
* @return string|null
*/
public function getState()
{
return $this->getProperty('state');
}
/**
* Returns the country component of the location
*
* @return string|null
*/
public function getCountry()
{
return $this->getProperty('country');
}
/**
* Returns the zipcode component of the location
*
* @return string|null
*/
public function getZip()
{
return $this->getProperty('zip');
}
/**
* Returns the latitude component of the location
*
* @return float|null
*/
public function getLatitude()
{
return $this->getProperty('latitude');
}
/**
* Returns the street component of the location
*
* @return float|null
*/
public function getLongitude()
{
return $this->getProperty('longitude');
}
}

View file

@ -0,0 +1,167 @@
<?php
/**
* Copyright 2014 Facebook, Inc.
*
* You are hereby granted a non-exclusive, worldwide, royalty-free license to
* use, copy, modify, and distribute this software in source code or binary
* form for use in connection with the web services and APIs provided by
* Facebook.
*
* As with any software that integrates with the Facebook platform, your use
* of this software is subject to the Facebook Developer Principles and
* Policies [http://developers.facebook.com/policy/]. This copyright notice
* shall be included in all copies or substantial portions of the software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
* THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
* DEALINGS IN THE SOFTWARE.
*
*/
namespace Facebook;
/**
* Class GraphObject
* @package Facebook
* @author Fosco Marotto <fjm@fb.com>
* @author David Poll <depoll@fb.com>
*/
class GraphObject
{
/**
* @var array - Holds the raw associative data for this object
*/
protected $backingData;
/**
* Creates a GraphObject using the data provided.
*
* @param array $raw
*/
public function __construct($raw)
{
if ($raw instanceof \stdClass) {
$raw = get_object_vars($raw);
}
$this->backingData = $raw;
if (isset($this->backingData['data']) && count($this->backingData) === 1) {
$this->backingData = $this->backingData['data'];
}
}
/**
* cast - Return a new instance of a FacebookGraphObject subclass for this
* objects underlying data.
*
* @param string $type The GraphObject subclass to cast to
*
* @return GraphObject
*
* @throws FacebookSDKException
*/
public function cast($type)
{
if ($this instanceof $type) {
return $this;
}
if (is_subclass_of($type, GraphObject::className())) {
return new $type($this->backingData);
} else {
throw new FacebookSDKException(
'Cannot cast to an object that is not a GraphObject subclass', 620
);
}
}
/**
* asArray - Return a key-value associative array for the given graph object.
*
* @return array
*/
public function asArray()
{
return $this->backingData;
}
/**
* getProperty - Gets the value of the named property for this graph object,
* cast to the appropriate subclass type if provided.
*
* @param string $name The property to retrieve
* @param string $type The subclass of GraphObject, optionally
*
* @return mixed
*/
public function getProperty($name, $type = 'Facebook\GraphObject')
{
if (isset($this->backingData[$name])) {
$value = $this->backingData[$name];
if (is_scalar($value)) {
return $value;
} else {
return (new GraphObject($value))->cast($type);
}
} else {
return null;
}
}
/**
* getPropertyAsArray - Get the list value of a named property for this graph
* object, where each item has been cast to the appropriate subclass type
* if provided.
*
* Calling this for a property that is not an array, the behavior
* is undefined, so dont do this.
*
* @param string $name The property to retrieve
* @param string $type The subclass of GraphObject, optionally
*
* @return array
*/
public function getPropertyAsArray($name, $type = 'Facebook\GraphObject')
{
$target = array();
if (isset($this->backingData[$name]['data'])) {
$target = $this->backingData[$name]['data'];
} else if (isset($this->backingData[$name])
&& !is_scalar($this->backingData[$name])) {
$target = $this->backingData[$name];
}
$out = array();
foreach ($target as $key => $value) {
if (is_scalar($value)) {
$out[$key] = $value;
} else {
$out[$key] = (new GraphObject($value))->cast($type);
}
}
return $out;
}
/**
* getPropertyNames - Returns a list of all properties set on the object.
*
* @return array
*/
public function getPropertyNames()
{
return array_keys($this->backingData);
}
/**
* Returns the string class name of the GraphObject or subclass.
*
* @return string
*/
public static function className()
{
return get_called_class();
}
}

View file

@ -0,0 +1,64 @@
<?php
/**
* Copyright 2014 Facebook, Inc.
*
* You are hereby granted a non-exclusive, worldwide, royalty-free license to
* use, copy, modify, and distribute this software in source code or binary
* form for use in connection with the web services and APIs provided by
* Facebook.
*
* As with any software that integrates with the Facebook platform, your use
* of this software is subject to the Facebook Developer Principles and
* Policies [http://developers.facebook.com/policy/]. This copyright notice
* shall be included in all copies or substantial portions of the software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
* THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
* DEALINGS IN THE SOFTWARE.
*
*/
namespace Facebook;
/**
* Class GraphPage
* @package Facebook
* @author Artur Luiz <artur@arturluiz.com.br>
*/
class GraphPage extends GraphObject
{
/**
* Returns the ID for the user's page as a string if present.
*
* @return string|null
*/
public function getId()
{
return $this->getProperty('id');
}
/**
* Returns the Category for the user's page as a string if present.
*
* @return string|null
*/
public function getCategory()
{
return $this->getProperty('category');
}
/**
* Returns the Name of the user's page as a string if present.
*
* @return string|null
*/
public function getName()
{
return $this->getProperty('name');
}
}

View file

@ -0,0 +1,115 @@
<?php
/**
* Copyright 2014 Facebook, Inc.
*
* You are hereby granted a non-exclusive, worldwide, royalty-free license to
* use, copy, modify, and distribute this software in source code or binary
* form for use in connection with the web services and APIs provided by
* Facebook.
*
* As with any software that integrates with the Facebook platform, your use
* of this software is subject to the Facebook Developer Principles and
* Policies [http://developers.facebook.com/policy/]. This copyright notice
* shall be included in all copies or substantial portions of the software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
* THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
* DEALINGS IN THE SOFTWARE.
*
*/
namespace Facebook;
/**
* Class GraphSessionInfo
* @package Facebook
* @author Fosco Marotto <fjm@fb.com>
* @author David Poll <depoll@fb.com>
*/
class GraphSessionInfo extends GraphObject
{
/**
* Returns the application id the token was issued for.
*
* @return string|null
*/
public function getAppId()
{
return $this->getProperty('app_id');
}
/**
* Returns the application name the token was issued for.
*
* @return string|null
*/
public function getApplication()
{
return $this->getProperty('application');
}
/**
* Returns the date & time that the token expires.
*
* @return \DateTime|null
*/
public function getExpiresAt()
{
$stamp = $this->getProperty('expires_at');
if ($stamp) {
return (new \DateTime())->setTimestamp($stamp);
} else {
return null;
}
}
/**
* Returns whether the token is valid.
*
* @return boolean
*/
public function isValid()
{
return $this->getProperty('is_valid');
}
/**
* Returns the date & time the token was issued at.
*
* @return \DateTime|null
*/
public function getIssuedAt()
{
$stamp = $this->getProperty('issued_at');
if ($stamp) {
return (new \DateTime())->setTimestamp($stamp);
} else {
return null;
}
}
/**
* Returns the scope permissions associated with the token.
*
* @return array
*/
public function getScopes()
{
return $this->getPropertyAsArray('scopes');
}
/**
* Returns the login id of the user associated with the token.
*
* @return string|null
*/
public function getId()
{
return $this->getProperty('user_id');
}
}

View file

@ -0,0 +1,120 @@
<?php
/**
* Copyright 2014 Facebook, Inc.
*
* You are hereby granted a non-exclusive, worldwide, royalty-free license to
* use, copy, modify, and distribute this software in source code or binary
* form for use in connection with the web services and APIs provided by
* Facebook.
*
* As with any software that integrates with the Facebook platform, your use
* of this software is subject to the Facebook Developer Principles and
* Policies [http://developers.facebook.com/policy/]. This copyright notice
* shall be included in all copies or substantial portions of the software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
* THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
* DEALINGS IN THE SOFTWARE.
*
*/
namespace Facebook;
/**
* Class GraphUser
* @package Facebook
* @author Fosco Marotto <fjm@fb.com>
* @author David Poll <depoll@fb.com>
*/
class GraphUser extends GraphObject
{
/**
* Returns the ID for the user as a string if present.
*
* @return string|null
*/
public function getId()
{
return $this->getProperty('id');
}
/**
* Returns the name for the user as a string if present.
*
* @return string|null
*/
public function getName()
{
return $this->getProperty('name');
}
/**
* Returns the first name for the user as a string if present.
*
* @return string|null
*/
public function getFirstName()
{
return $this->getProperty('first_name');
}
/**
* Returns the middle name for the user as a string if present.
*
* @return string|null
*/
public function getMiddleName()
{
return $this->getProperty('middle_name');
}
/**
* Returns the last name for the user as a string if present.
*
* @return string|null
*/
public function getLastName()
{
return $this->getProperty('last_name');
}
/**
* Returns the Facebook URL for the user as a string if available.
*
* @return string|null
*/
public function getLink()
{
return $this->getProperty('link');
}
/**
* Returns the users birthday, if available.
*
* @return \DateTime|null
*/
public function getBirthday()
{
$value = $this->getProperty('birthday');
if ($value) {
return new \DateTime($value);
}
return null;
}
/**
* Returns the current location of the user as a FacebookGraphLocation
* if available.
*
* @return GraphLocation|null
*/
public function getLocation()
{
return $this->getProperty('location', GraphLocation::className());
}
}

View file

@ -0,0 +1,84 @@
<?php
/**
* Copyright 2014 Facebook, Inc.
*
* You are hereby granted a non-exclusive, worldwide, royalty-free license to
* use, copy, modify, and distribute this software in source code or binary
* form for use in connection with the web services and APIs provided by
* Facebook.
*
* As with any software that integrates with the Facebook platform, your use
* of this software is subject to the Facebook Developer Principles and
* Policies [http://developers.facebook.com/policy/]. This copyright notice
* shall be included in all copies or substantial portions of the software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
* THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
* DEALINGS IN THE SOFTWARE.
*
*/
namespace Facebook;
/**
* Class GraphUserPage
* @package Facebook
* @author Artur Luiz <artur@arturluiz.com.br>
*/
class GraphUserPage extends GraphObject
{
/**
* Returns the ID for the user's page as a string if present.
*
* @return string|null
*/
public function getId()
{
return $this->getProperty('id');
}
/**
* Returns the Category for the user's page as a string if present.
*
* @return string|null
*/
public function getCategory()
{
return $this->getProperty('category');
}
/**
* Returns the Name of the user's page as a string if present.
*
* @return string|null
*/
public function getName()
{
return $this->getProperty('name');
}
/**
* Returns the Access Token used to access the user's page as a string if present.
*
* @return string|null
*/
public function getAccessToken()
{
return $this->getProperty('access_token');
}
/**
* Returns the Permissions for the user's page as an array if present.
*
* @return array|null
*/
public function getPermissions()
{
return $this->getProperty('perms');
}
}

View file

@ -0,0 +1,129 @@
<?php
/**
* Copyright 2014 Facebook, Inc.
*
* You are hereby granted a non-exclusive, worldwide, royalty-free license to
* use, copy, modify, and distribute this software in source code or binary
* form for use in connection with the web services and APIs provided by
* Facebook.
*
* As with any software that integrates with the Facebook platform, your use
* of this software is subject to the Facebook Developer Principles and
* Policies [http://developers.facebook.com/policy/]. This copyright notice
* shall be included in all copies or substantial portions of the software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
* THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
* DEALINGS IN THE SOFTWARE.
*
*/
namespace Facebook\HttpClients;
/**
* Class FacebookCurl
* Abstraction for the procedural curl elements so that curl can be mocked
* and the implementation can be tested.
* @package Facebook
*/
class FacebookCurl
{
/**
* @var resource Curl resource instance
*/
protected $curl;
/**
* Make a new curl reference instance
*/
public function init()
{
$this->curl = curl_init();
}
/**
* Set a curl option
*
* @param $key
* @param $value
*/
public function setopt($key, $value)
{
curl_setopt($this->curl, $key, $value);
}
/**
* Set an array of options to a curl resource
*
* @param array $options
*/
public function setopt_array(array $options)
{
curl_setopt_array($this->curl, $options);
}
/**
* Send a curl request
*
* @return mixed
*/
public function exec()
{
return curl_exec($this->curl);
}
/**
* Return the curl error number
*
* @return int
*/
public function errno()
{
return curl_errno($this->curl);
}
/**
* Return the curl error message
*
* @return string
*/
public function error()
{
return curl_error($this->curl);
}
/**
* Get info from a curl reference
*
* @param $type
*
* @return mixed
*/
public function getinfo($type)
{
return curl_getinfo($this->curl, $type);
}
/**
* Get the currently installed curl version
*
* @return array
*/
public function version()
{
return curl_version();
}
/**
* Close the resource connection to curl
*/
public function close()
{
curl_close($this->curl);
}
}

View file

@ -0,0 +1,323 @@
<?php
/**
* Copyright 2014 Facebook, Inc.
*
* You are hereby granted a non-exclusive, worldwide, royalty-free license to
* use, copy, modify, and distribute this software in source code or binary
* form for use in connection with the web services and APIs provided by
* Facebook.
*
* As with any software that integrates with the Facebook platform, your use
* of this software is subject to the Facebook Developer Principles and
* Policies [http://developers.facebook.com/policy/]. This copyright notice
* shall be included in all copies or substantial portions of the software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
* THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
* DEALINGS IN THE SOFTWARE.
*
*/
namespace Facebook\HttpClients;
use Facebook\FacebookSDKException;
/**
* Class FacebookCurlHttpClient
* @package Facebook
*/
class FacebookCurlHttpClient implements FacebookHttpable
{
/**
* @var array The headers to be sent with the request
*/
protected $requestHeaders = array();
/**
* @var array The headers received from the response
*/
protected $responseHeaders = array();
/**
* @var int The HTTP status code returned from the server
*/
protected $responseHttpStatusCode = 0;
/**
* @var string The client error message
*/
protected $curlErrorMessage = '';
/**
* @var int The curl client error code
*/
protected $curlErrorCode = 0;
/**
* @var string|boolean The raw response from the server
*/
protected $rawResponse;
/**
* @var FacebookCurl Procedural curl as object
*/
protected static $facebookCurl;
/**
* @const Curl Version which is unaffected by the proxy header length error.
*/
const CURL_PROXY_QUIRK_VER = 0x071E00;
/**
* @const "Connection Established" header text
*/
const CONNECTION_ESTABLISHED = "HTTP/1.0 200 Connection established\r\n\r\n";
/**
* @param FacebookCurl|null Procedural curl as object
*/
public function __construct(FacebookCurl $facebookCurl = null)
{
self::$facebookCurl = $facebookCurl ?: new FacebookCurl();
}
/**
* The headers we want to send with the request
*
* @param string $key
* @param string $value
*/
public function addRequestHeader($key, $value)
{
$this->requestHeaders[$key] = $value;
}
/**
* The headers returned in the response
*
* @return array
*/
public function getResponseHeaders()
{
return $this->responseHeaders;
}
/**
* The HTTP status response code
*
* @return int
*/
public function getResponseHttpStatusCode()
{
return $this->responseHttpStatusCode;
}
/**
* Sends a request to the server
*
* @param string $url The endpoint to send the request to
* @param string $method The request method
* @param array $parameters The key value pairs to be sent in the body
*
* @return string Raw response from the server
*
* @throws \Facebook\FacebookSDKException
*/
public function send($url, $method = 'GET', $parameters = array())
{
$this->openConnection($url, $method, $parameters);
$this->tryToSendRequest();
// Need to verify the peer
if ($this->curlErrorCode == 60 || $this->curlErrorCode == 77) {
$this->addBundledCert();
$this->tryToSendRequest();
}
if ($this->curlErrorCode) {
throw new FacebookSDKException($this->curlErrorMessage, $this->curlErrorCode);
}
// Separate the raw headers from the raw body
list($rawHeaders, $rawBody) = $this->extractResponseHeadersAndBody();
$this->responseHeaders = self::headersToArray($rawHeaders);
$this->closeConnection();
return $rawBody;
}
/**
* Opens a new curl connection
*
* @param string $url The endpoint to send the request to
* @param string $method The request method
* @param array $parameters The key value pairs to be sent in the body
*/
public function openConnection($url, $method = 'GET', $parameters = array())
{
$options = array(
CURLOPT_URL => $url,
CURLOPT_CONNECTTIMEOUT => 10,
CURLOPT_TIMEOUT => 60,
CURLOPT_RETURNTRANSFER => true, // Follow 301 redirects
CURLOPT_HEADER => true, // Enable header processing
);
if ($method !== "GET") {
$options[CURLOPT_POSTFIELDS] = $parameters;
}
if ($method === 'DELETE' || $method === 'PUT') {
$options[CURLOPT_CUSTOMREQUEST] = $method;
}
if (!empty($this->requestHeaders)) {
$options[CURLOPT_HTTPHEADER] = $this->compileRequestHeaders();
}
self::$facebookCurl->init();
self::$facebookCurl->setopt_array($options);
}
/**
* Add a bundled cert to the connection
*/
public function addBundledCert()
{
self::$facebookCurl->setopt(CURLOPT_CAINFO,
dirname(__FILE__) . DIRECTORY_SEPARATOR . 'fb_ca_chain_bundle.crt');
}
/**
* Closes an existing curl connection
*/
public function closeConnection()
{
self::$facebookCurl->close();
}
/**
* Try to send the request
*/
public function tryToSendRequest()
{
$this->sendRequest();
$this->curlErrorMessage = self::$facebookCurl->error();
$this->curlErrorCode = self::$facebookCurl->errno();
$this->responseHttpStatusCode = self::$facebookCurl->getinfo(CURLINFO_HTTP_CODE);
}
/**
* Send the request and get the raw response from curl
*/
public function sendRequest()
{
$this->rawResponse = self::$facebookCurl->exec();
}
/**
* Compiles the request headers into a curl-friendly format
*
* @return array
*/
public function compileRequestHeaders()
{
$return = array();
foreach ($this->requestHeaders as $key => $value) {
$return[] = $key . ': ' . $value;
}
return $return;
}
/**
* Extracts the headers and the body into a two-part array
*
* @return array
*/
public function extractResponseHeadersAndBody()
{
$headerSize = self::getHeaderSize();
$rawHeaders = mb_substr($this->rawResponse, 0, $headerSize);
$rawBody = mb_substr($this->rawResponse, $headerSize);
return array(trim($rawHeaders), trim($rawBody));
}
/**
* Converts raw header responses into an array
*
* @param string $rawHeaders
*
* @return array
*/
public static function headersToArray($rawHeaders)
{
$headers = array();
// Normalize line breaks
$rawHeaders = str_replace("\r\n", "\n", $rawHeaders);
// There will be multiple headers if a 301 was followed
// or a proxy was followed, etc
$headerCollection = explode("\n\n", trim($rawHeaders));
// We just want the last response (at the end)
$rawHeader = array_pop($headerCollection);
$headerComponents = explode("\n", $rawHeader);
foreach ($headerComponents as $line) {
if (strpos($line, ': ') === false) {
$headers['http_code'] = $line;
} else {
list ($key, $value) = explode(': ', $line);
$headers[$key] = $value;
}
}
return $headers;
}
/**
* Return proper header size
*
* @return integer
*/
private function getHeaderSize()
{
$headerSize = self::$facebookCurl->getinfo(CURLINFO_HEADER_SIZE);
// This corrects a Curl bug where header size does not account
// for additional Proxy headers.
if ( self::needsCurlProxyFix() ) {
// Additional way to calculate the request body size.
if (preg_match('/Content-Length: (\d+)/', $this->rawResponse, $m)) {
$headerSize = mb_strlen($this->rawResponse) - $m[1];
} elseif (stripos($this->rawResponse, self::CONNECTION_ESTABLISHED) !== false) {
$headerSize += mb_strlen(self::CONNECTION_ESTABLISHED);
}
}
return $headerSize;
}
/**
* Detect versions of Curl which report incorrect header lengths when
* using Proxies.
*
* @return boolean
*/
private static function needsCurlProxyFix()
{
$ver = self::$facebookCurl->version();
$version = $ver['version_number'];
return $version < self::CURL_PROXY_QUIRK_VER;
}
}

View file

@ -0,0 +1,132 @@
<?php
/**
* Copyright 2014 Facebook, Inc.
*
* You are hereby granted a non-exclusive, worldwide, royalty-free license to
* use, copy, modify, and distribute this software in source code or binary
* form for use in connection with the web services and APIs provided by
* Facebook.
*
* As with any software that integrates with the Facebook platform, your use
* of this software is subject to the Facebook Developer Principles and
* Policies [http://developers.facebook.com/policy/]. This copyright notice
* shall be included in all copies or substantial portions of the software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
* THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
* DEALINGS IN THE SOFTWARE.
*
*/
namespace Facebook\HttpClients;
use Facebook\FacebookSDKException;
use GuzzleHttp\Client;
use GuzzleHttp\Exception\AdapterException;
use GuzzleHttp\Exception\RequestException;
class FacebookGuzzleHttpClient implements FacebookHttpable {
/**
* @var array The headers to be sent with the request
*/
protected $requestHeaders = array();
/**
* @var array The headers received from the response
*/
protected $responseHeaders = array();
/**
* @var int The HTTP status code returned from the server
*/
protected $responseHttpStatusCode = 0;
/**
* @var \GuzzleHttp\Client The Guzzle client
*/
protected static $guzzleClient;
/**
* @param \GuzzleHttp\Client|null The Guzzle client
*/
public function __construct(Client $guzzleClient = null)
{
self::$guzzleClient = $guzzleClient ?: new Client();
}
/**
* The headers we want to send with the request
*
* @param string $key
* @param string $value
*/
public function addRequestHeader($key, $value)
{
$this->requestHeaders[$key] = $value;
}
/**
* The headers returned in the response
*
* @return array
*/
public function getResponseHeaders()
{
return $this->responseHeaders;
}
/**
* The HTTP status response code
*
* @return int
*/
public function getResponseHttpStatusCode()
{
return $this->responseHttpStatusCode;
}
/**
* Sends a request to the server
*
* @param string $url The endpoint to send the request to
* @param string $method The request method
* @param array $parameters The key value pairs to be sent in the body
*
* @return string Raw response from the server
*
* @throws \Facebook\FacebookSDKException
*/
public function send($url, $method = 'GET', $parameters = array())
{
$options = array();
if ($parameters) {
$options = array('body' => $parameters);
}
$request = self::$guzzleClient->createRequest($method, $url, $options);
foreach($this->requestHeaders as $k => $v) {
$request->setHeader($k, $v);
}
try {
$rawResponse = self::$guzzleClient->send($request);
} catch (RequestException $e) {
if ($e->getPrevious() instanceof AdapterException) {
throw new FacebookSDKException($e->getMessage(), $e->getCode());
}
$rawResponse = $e->getResponse();
}
$this->responseHttpStatusCode = $rawResponse->getStatusCode();
$this->responseHeaders = $rawResponse->getHeaders();
return $rawResponse->getBody();
}
}

View file

@ -0,0 +1,68 @@
<?php
/**
* Copyright 2014 Facebook, Inc.
*
* You are hereby granted a non-exclusive, worldwide, royalty-free license to
* use, copy, modify, and distribute this software in source code or binary
* form for use in connection with the web services and APIs provided by
* Facebook.
*
* As with any software that integrates with the Facebook platform, your use
* of this software is subject to the Facebook Developer Principles and
* Policies [http://developers.facebook.com/policy/]. This copyright notice
* shall be included in all copies or substantial portions of the software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
* THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
* DEALINGS IN THE SOFTWARE.
*
*/
namespace Facebook\HttpClients;
/**
* Interface FacebookHttpable
* @package Facebook
*/
interface FacebookHttpable
{
/**
* The headers we want to send with the request
*
* @param string $key
* @param string $value
*/
public function addRequestHeader($key, $value);
/**
* The headers returned in the response
*
* @return array
*/
public function getResponseHeaders();
/**
* The HTTP status response code
*
* @return int
*/
public function getResponseHttpStatusCode();
/**
* Sends a request to the server
*
* @param string $url The endpoint to send the request to
* @param string $method The request method
* @param array $parameters The key value pairs to be sent in the body
*
* @return string Raw response from the server
*
* @throws \Facebook\FacebookSDKException
*/
public function send($url, $method = 'GET', $parameters = array());
}

View file

@ -0,0 +1,79 @@
<?php
/**
* Copyright 2014 Facebook, Inc.
*
* You are hereby granted a non-exclusive, worldwide, royalty-free license to
* use, copy, modify, and distribute this software in source code or binary
* form for use in connection with the web services and APIs provided by
* Facebook.
*
* As with any software that integrates with the Facebook platform, your use
* of this software is subject to the Facebook Developer Principles and
* Policies [http://developers.facebook.com/policy/]. This copyright notice
* shall be included in all copies or substantial portions of the software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
* THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
* DEALINGS IN THE SOFTWARE.
*
*/
namespace Facebook\HttpClients;
/**
* Class FacebookStream
* Abstraction for the procedural stream elements so that the functions can be
* mocked and the implementation can be tested.
* @package Facebook
*/
class FacebookStream
{
/**
* @var resource Context stream resource instance
*/
protected $stream;
/**
* @var array Response headers from the stream wrapper
*/
protected $responseHeaders;
/**
* Make a new context stream reference instance
*
* @param array $options
*/
public function streamContextCreate(array $options)
{
$this->stream = stream_context_create($options);
}
/**
* The response headers from the stream wrapper
*
* @return array|null
*/
public function getResponseHeaders()
{
return $this->responseHeaders;
}
/**
* Send a stream wrapped request
*
* @param string $url
*
* @return mixed
*/
public function fileGetContents($url)
{
$rawResponse = file_get_contents($url, false, $this->stream);
$this->responseHeaders = $http_response_header;
return $rawResponse;
}
}

View file

@ -0,0 +1,188 @@
<?php
/**
* Copyright 2014 Facebook, Inc.
*
* You are hereby granted a non-exclusive, worldwide, royalty-free license to
* use, copy, modify, and distribute this software in source code or binary
* form for use in connection with the web services and APIs provided by
* Facebook.
*
* As with any software that integrates with the Facebook platform, your use
* of this software is subject to the Facebook Developer Principles and
* Policies [http://developers.facebook.com/policy/]. This copyright notice
* shall be included in all copies or substantial portions of the software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
* THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
* DEALINGS IN THE SOFTWARE.
*
*/
namespace Facebook\HttpClients;
use Facebook\FacebookSDKException;
class FacebookStreamHttpClient implements FacebookHttpable {
/**
* @var array The headers to be sent with the request
*/
protected $requestHeaders = array();
/**
* @var array The headers received from the response
*/
protected $responseHeaders = array();
/**
* @var int The HTTP status code returned from the server
*/
protected $responseHttpStatusCode = 0;
/**
* @var FacebookStream Procedural stream wrapper as object
*/
protected static $facebookStream;
/**
* @param FacebookStream|null Procedural stream wrapper as object
*/
public function __construct(FacebookStream $facebookStream = null)
{
self::$facebookStream = $facebookStream ?: new FacebookStream();
}
/**
* The headers we want to send with the request
*
* @param string $key
* @param string $value
*/
public function addRequestHeader($key, $value)
{
$this->requestHeaders[$key] = $value;
}
/**
* The headers returned in the response
*
* @return array
*/
public function getResponseHeaders()
{
return $this->responseHeaders;
}
/**
* The HTTP status response code
*
* @return int
*/
public function getResponseHttpStatusCode()
{
return $this->responseHttpStatusCode;
}
/**
* Sends a request to the server
*
* @param string $url The endpoint to send the request to
* @param string $method The request method
* @param array $parameters The key value pairs to be sent in the body
*
* @return string Raw response from the server
*
* @throws \Facebook\FacebookSDKException
*/
public function send($url, $method = 'GET', $parameters = array())
{
$options = array(
'http' => array(
'method' => $method,
'timeout' => 60,
'ignore_errors' => true
),
'ssl' => array(
'verify_peer' => true,
'cafile' => dirname(__FILE__) . DIRECTORY_SEPARATOR . 'fb_ca_chain_bundle.crt',
),
);
if ($parameters) {
$options['http']['content'] = http_build_query($parameters, null, '&');
$this->addRequestHeader('Content-type', 'application/x-www-form-urlencoded');
}
$options['http']['header'] = $this->compileHeader();
self::$facebookStream->streamContextCreate($options);
$rawResponse = self::$facebookStream->fileGetContents($url);
$rawHeaders = self::$facebookStream->getResponseHeaders();
if ($rawResponse === false || !$rawHeaders) {
throw new FacebookSDKException('Stream returned an empty response', 660);
}
$this->responseHeaders = self::formatHeadersToArray($rawHeaders);
$this->responseHttpStatusCode = self::getStatusCodeFromHeader($this->responseHeaders['http_code']);
return $rawResponse;
}
/**
* Formats the headers for use in the stream wrapper
*
* @return string
*/
public function compileHeader()
{
$header = [];
foreach($this->requestHeaders as $k => $v) {
$header[] = $k . ': ' . $v;
}
return implode("\r\n", $header);
}
/**
* Converts array of headers returned from the wrapper into
* something standard
*
* @param array $rawHeaders
*
* @return array
*/
public static function formatHeadersToArray(array $rawHeaders)
{
$headers = array();
foreach ($rawHeaders as $line) {
if (strpos($line, ':') === false) {
$headers['http_code'] = $line;
} else {
list ($key, $value) = explode(': ', $line);
$headers[$key] = $value;
}
}
return $headers;
}
/**
* Pulls out the HTTP status code from a response header
*
* @param string $header
*
* @return int
*/
public static function getStatusCodeFromHeader($header)
{
preg_match('|HTTP/\d\.\d\s+(\d+)\s+.*|', $header, $match);
return (int) $match[1];
}
}

View file

@ -0,0 +1,23 @@
<?php
namespace GuzzleHttp\Adapter;
use GuzzleHttp\Message\ResponseInterface;
/**
* Adapter interface used to transfer HTTP requests.
*
* @link http://docs.guzzlephp.org/en/guzzle4/adapters.html for a full
* explanation of adapters and their responsibilities.
*/
interface AdapterInterface
{
/**
* Transfers an HTTP request and populates a response
*
* @param TransactionInterface $transaction Transaction abject to populate
*
* @return ResponseInterface
*/
public function send(TransactionInterface $transaction);
}

View file

@ -0,0 +1,158 @@
<?php
namespace GuzzleHttp\Adapter\Curl;
use GuzzleHttp\Adapter\TransactionInterface;
use GuzzleHttp\Exception\AdapterException;
/**
* Provides context for a Curl transaction, including active handles,
* pending transactions, and whether or not this is a batch or single
* transaction.
*/
class BatchContext
{
/** @var resource Curl multi resource */
private $multi;
/** @var \SplObjectStorage Map of transactions to curl resources */
private $handles;
/** @var \Iterator Yields pending transactions */
private $pending;
/** @var bool Whether or not to throw transactions */
private $throwsExceptions;
/**
* @param resource $multiHandle Initialized curl_multi resource
* @param bool $throwsExceptions Whether or not exceptions are thrown
* @param \Iterator $pending Iterator yielding pending transactions
*/
public function __construct(
$multiHandle,
$throwsExceptions,
\Iterator $pending = null
) {
$this->multi = $multiHandle;
$this->handles = new \SplObjectStorage();
$this->throwsExceptions = $throwsExceptions;
$this->pending = $pending;
}
/**
* Find a transaction for a given curl handle
*
* @param resource $handle Curl handle
*
* @return TransactionInterface
* @throws AdapterException if a transaction is not found
*/
public function findTransaction($handle)
{
foreach ($this->handles as $transaction) {
if ($this->handles[$transaction] === $handle) {
return $transaction;
}
}
throw new AdapterException('No curl handle was found');
}
/**
* Returns true if there are any remaining pending transactions
*
* @return bool
*/
public function hasPending()
{
return $this->pending && $this->pending->valid();
}
/**
* Pop the next transaction from the transaction queue
*
* @return TransactionInterface|null
*/
public function nextPending()
{
if (!$this->hasPending()) {
return null;
}
$current = $this->pending->current();
$this->pending->next();
return $current;
}
/**
* Checks if the batch is to throw exceptions on error
*
* @return bool
*/
public function throwsExceptions()
{
return $this->throwsExceptions;
}
/**
* Get the curl_multi handle
*
* @return resource
*/
public function getMultiHandle()
{
return $this->multi;
}
/**
* Add a transaction to the multi handle
*
* @param TransactionInterface $transaction Transaction to add
* @param resource $handle Resource to use with the handle
*
* @throws AdapterException If the handle is already registered
*/
public function addTransaction(TransactionInterface $transaction, $handle)
{
if (isset($this->handles[$transaction])) {
throw new AdapterException('Transaction already registered');
}
$code = curl_multi_add_handle($this->multi, $handle);
if ($code != CURLM_OK) {
MultiAdapter::throwMultiError($code);
}
$this->handles[$transaction] = $handle;
}
/**
* Remove a transaction and associated handle from the context
*
* @param TransactionInterface $transaction Transaction to remove
*
* @return array Returns the curl_getinfo array
* @throws AdapterException if the transaction is not found
*/
public function removeTransaction(TransactionInterface $transaction)
{
if (!isset($this->handles[$transaction])) {
throw new AdapterException('Transaction not registered');
}
$handle = $this->handles[$transaction];
$code = curl_multi_remove_handle($this->multi, $handle);
if ($code != CURLM_OK) {
MultiAdapter::throwMultiError($code);
}
$info = curl_getinfo($handle);
curl_close($handle);
unset($this->handles[$transaction]);
return $info;
}
}

View file

@ -0,0 +1,142 @@
<?php
namespace GuzzleHttp\Adapter\Curl;
use GuzzleHttp\Adapter\AdapterInterface;
use GuzzleHttp\Adapter\TransactionInterface;
use GuzzleHttp\Event\RequestEvents;
use GuzzleHttp\Exception\AdapterException;
use GuzzleHttp\Message\MessageFactoryInterface;
/**
* HTTP adapter that uses cURL easy handles as a transport layer.
*
* Requires PHP 5.5+
*
* When using the CurlAdapter, custom curl options can be specified as an
* associative array of curl option constants mapping to values in the
* **curl** key of a request's configuration options.
*/
class CurlAdapter implements AdapterInterface
{
/** @var CurlFactory */
private $curlFactory;
/** @var MessageFactoryInterface */
private $messageFactory;
/** @var array Array of curl easy handles */
private $handles = [];
/** @var array Array of owned curl easy handles */
private $ownedHandles = [];
/** @var int Total number of idle handles to keep in cache */
private $maxHandles;
/**
* Accepts an associative array of options:
*
* - handle_factory: Optional callable factory used to create cURL handles.
* The callable is invoked with the following arguments:
* TransactionInterface, MessageFactoryInterface, and an optional cURL
* handle to modify. The factory method must then return a cURL resource.
* - max_handles: Maximum number of idle handles (defaults to 5).
*
* @param MessageFactoryInterface $messageFactory
* @param array $options Array of options to use with the adapter
*/
public function __construct(
MessageFactoryInterface $messageFactory,
array $options = []
) {
$this->handles = $this->ownedHandles = [];
$this->messageFactory = $messageFactory;
$this->curlFactory = isset($options['handle_factory'])
? $options['handle_factory']
: new CurlFactory();
$this->maxHandles = isset($options['max_handles'])
? $options['max_handles']
: 5;
}
public function __destruct()
{
foreach ($this->handles as $handle) {
if (is_resource($handle)) {
curl_close($handle);
}
}
}
public function send(TransactionInterface $transaction)
{
RequestEvents::emitBefore($transaction);
if ($response = $transaction->getResponse()) {
return $response;
}
$factory = $this->curlFactory;
$handle = $factory(
$transaction,
$this->messageFactory,
$this->checkoutEasyHandle()
);
curl_exec($handle);
$info = curl_getinfo($handle);
$info['curl_result'] = curl_errno($handle);
if ($info['curl_result']) {
$this->handleError($transaction, $info, $handle);
} else {
$this->releaseEasyHandle($handle);
RequestEvents::emitComplete($transaction, $info);
}
return $transaction->getResponse();
}
private function handleError(
TransactionInterface $transaction,
$info,
$handle
) {
$error = curl_error($handle);
$this->releaseEasyHandle($handle);
RequestEvents::emitError(
$transaction,
new AdapterException("cURL error {$info['curl_result']}: {$error}"),
$info
);
}
private function checkoutEasyHandle()
{
// Find an unused handle in the cache
if (false !== ($key = array_search(false, $this->ownedHandles, true))) {
$this->ownedHandles[$key] = true;
return $this->handles[$key];
}
// Add a new handle
$handle = curl_init();
$id = (int) $handle;
$this->handles[$id] = $handle;
$this->ownedHandles[$id] = true;
return $handle;
}
private function releaseEasyHandle($handle)
{
$id = (int) $handle;
if (count($this->ownedHandles) > $this->maxHandles) {
curl_close($this->handles[$id]);
unset($this->handles[$id], $this->ownedHandles[$id]);
} else {
curl_reset($handle);
$this->ownedHandles[$id] = false;
}
}
}

View file

@ -0,0 +1,331 @@
<?php
namespace GuzzleHttp\Adapter\Curl;
use GuzzleHttp\Adapter\TransactionInterface;
use GuzzleHttp\Message\MessageFactoryInterface;
use GuzzleHttp\Message\RequestInterface;
use GuzzleHttp\Stream;
use GuzzleHttp\Exception\AdapterException;
/**
* Creates curl resources from a request and response object
*/
class CurlFactory
{
/**
* Creates a cURL handle based on a transaction.
*
* @param TransactionInterface $transaction Holds a request and response
* @param MessageFactoryInterface $messageFactory Used to create responses
* @param null|resource $handle Optionally provide a curl handle to modify
*
* @return resource Returns a prepared cURL handle
* @throws AdapterException when an option cannot be applied
*/
public function __invoke(
TransactionInterface $transaction,
MessageFactoryInterface $messageFactory,
$handle = null
) {
$request = $transaction->getRequest();
$mediator = new RequestMediator($transaction, $messageFactory);
$options = $this->getDefaultOptions($request, $mediator);
$this->applyMethod($request, $options);
$this->applyTransferOptions($request, $mediator, $options);
$this->applyHeaders($request, $options);
unset($options['_headers']);
// Add adapter options from the request's configuration options
if ($config = $request->getConfig()['curl']) {
$options = $this->applyCustomCurlOptions($config, $options);
}
if (!$handle) {
$handle = curl_init();
}
curl_setopt_array($handle, $options);
return $handle;
}
protected function getDefaultOptions(
RequestInterface $request,
RequestMediator $mediator
) {
$url = $request->getUrl();
// Strip fragment from URL. See:
// https://github.com/guzzle/guzzle/issues/453
if (($pos = strpos($url, '#')) !== false) {
$url = substr($url, 0, $pos);
}
$config = $request->getConfig();
$options = array(
CURLOPT_URL => $url,
CURLOPT_CONNECTTIMEOUT => $config['connect_timeout'] ?: 150,
CURLOPT_RETURNTRANSFER => false,
CURLOPT_HEADER => false,
CURLOPT_WRITEFUNCTION => array($mediator, 'writeResponseBody'),
CURLOPT_HEADERFUNCTION => array($mediator, 'receiveResponseHeader'),
CURLOPT_READFUNCTION => array($mediator, 'readRequestBody'),
CURLOPT_HTTP_VERSION => $request->getProtocolVersion() === '1.0'
? CURL_HTTP_VERSION_1_0 : CURL_HTTP_VERSION_1_1,
CURLOPT_SSL_VERIFYPEER => 1,
CURLOPT_SSL_VERIFYHOST => 2,
'_headers' => $request->getHeaders()
);
if (defined('CURLOPT_PROTOCOLS')) {
// Allow only HTTP and HTTPS protocols
$options[CURLOPT_PROTOCOLS] = CURLPROTO_HTTP | CURLPROTO_HTTPS;
}
// Add CURLOPT_ENCODING if Accept-Encoding header is provided
if ($request->hasHeader('Accept-Encoding')) {
$options[CURLOPT_ENCODING] = $request->getHeader('Accept-Encoding');
// Let cURL set the Accept-Encoding header. Without this change
// curl could add a duplicate value.
$this->removeHeader('Accept-Encoding', $options);
}
return $options;
}
private function applyMethod(RequestInterface $request, array &$options)
{
$method = $request->getMethod();
if ($method == 'HEAD') {
$options[CURLOPT_NOBODY] = true;
unset($options[CURLOPT_WRITEFUNCTION], $options[CURLOPT_READFUNCTION]);
} else {
$options[CURLOPT_CUSTOMREQUEST] = $method;
if (!$request->getBody()) {
unset($options[CURLOPT_READFUNCTION]);
} else {
$this->applyBody($request, $options);
}
}
}
private function applyBody(RequestInterface $request, array &$options)
{
if ($request->hasHeader('Content-Length')) {
$size = (int) $request->getHeader('Content-Length');
} else {
$size = null;
}
$request->getBody()->seek(0);
// You can send the body as a string using curl's CURLOPT_POSTFIELDS
if (($size !== null && $size < 32768) ||
isset($request->getConfig()['curl']['body_as_string'])
) {
$options[CURLOPT_POSTFIELDS] = $request->getBody()->getContents();
// Don't duplicate the Content-Length header
$this->removeHeader('Content-Length', $options);
$this->removeHeader('Transfer-Encoding', $options);
} else {
$options[CURLOPT_UPLOAD] = true;
// Let cURL handle setting the Content-Length header
if ($size !== null) {
$options[CURLOPT_INFILESIZE] = $size;
$this->removeHeader('Content-Length', $options);
}
}
// If the Expect header is not present, prevent curl from adding it
if (!$request->hasHeader('Expect')) {
$options[CURLOPT_HTTPHEADER][] = 'Expect:';
}
}
private function applyHeaders(RequestInterface $request, array &$options)
{
foreach ($options['_headers'] as $name => $values) {
$options[CURLOPT_HTTPHEADER][] = $name . ': ' . implode(', ', $values);
}
// Remove the Expect header if one was not set
if (!$request->hasHeader('Accept')) {
$options[CURLOPT_HTTPHEADER][] = 'Accept:';
}
}
private function applyTransferOptions(
RequestInterface $request,
RequestMediator $mediator,
array &$options
) {
static $methods;
if (!$methods) {
$methods = array_flip(get_class_methods(__CLASS__));
}
foreach ($request->getConfig()->toArray() as $key => $value) {
$method = "add_{$key}";
if (isset($methods[$method])) {
$this->{$method}($request, $mediator, $options, $value);
}
}
}
private function add_debug(
RequestInterface $request,
RequestMediator $mediator,
&$options,
$value
) {
if ($value) {
$options[CURLOPT_STDERR] = is_resource($value) ? $value : STDOUT;
$options[CURLOPT_VERBOSE] = true;
}
}
private function add_proxy(
RequestInterface $request,
RequestMediator $mediator,
&$options,
$value
) {
if (!is_array($value)) {
$options[CURLOPT_PROXY] = $value;
} else {
$scheme = $request->getScheme();
if (isset($value[$scheme])) {
$options[CURLOPT_PROXY] = $value[$scheme];
}
}
}
private function add_timeout(
RequestInterface $request,
RequestMediator $mediator,
&$options,
$value
) {
$options[CURLOPT_TIMEOUT_MS] = $value * 1000;
}
private function add_connect_timeout(
RequestInterface $request,
RequestMediator $mediator,
&$options,
$value
) {
$options[CURLOPT_CONNECTTIMEOUT_MS] = $value * 1000;
}
private function add_verify(
RequestInterface $request,
RequestMediator $mediator,
&$options,
$value
) {
if ($value === false) {
unset($options[CURLOPT_CAINFO]);
$options[CURLOPT_SSL_VERIFYHOST] = 0;
$options[CURLOPT_SSL_VERIFYPEER] = false;
} elseif ($value === true || is_string($value)) {
$options[CURLOPT_SSL_VERIFYHOST] = 2;
$options[CURLOPT_SSL_VERIFYPEER] = true;
if ($value !== true) {
if (!file_exists($value)) {
throw new AdapterException('SSL certificate authority file'
. " not found: {$value}");
}
$options[CURLOPT_CAINFO] = $value;
}
}
}
private function add_cert(
RequestInterface $request,
RequestMediator $mediator,
&$options,
$value
) {
if (!file_exists($value)) {
throw new AdapterException("SSL certificate not found: {$value}");
}
$options[CURLOPT_SSLCERT] = $value;
}
private function add_ssl_key(
RequestInterface $request,
RequestMediator $mediator,
&$options,
$value
) {
if (is_array($value)) {
$options[CURLOPT_SSLKEYPASSWD] = $value[1];
$value = $value[0];
}
if (!file_exists($value)) {
throw new AdapterException("SSL private key not found: {$value}");
}
$options[CURLOPT_SSLKEY] = $value;
}
private function add_save_to(
RequestInterface $request,
RequestMediator $mediator,
&$options,
$value
) {
$mediator->setResponseBody(is_string($value)
? Stream\create(fopen($value, 'w'))
: Stream\create($value));
}
/**
* Takes an array of curl options specified in the 'curl' option of a
* request's configuration array and maps them to CURLOPT_* options.
*
* This method is only called when a request has a 'curl' config setting.
* Array key strings that start with CURL that have a matching constant
* value will be automatically converted to the matching constant.
*
* @param array $config Configuration array of custom curl option
* @param array $options Array of existing curl options
*
* @return array Returns a new array of curl options
*/
private function applyCustomCurlOptions(array $config, array $options)
{
unset($config['body_as_string']);
$curlOptions = [];
// Map curl constant strings to defined values
foreach ($config as $key => $value) {
if (defined($key) && substr($key, 0, 4) === 'CURL') {
$key = constant($key);
}
$curlOptions[$key] = $value;
}
return $curlOptions + $options;
}
/**
* Remove a header from the options array
*
* @param string $name Case-insensitive header to remove
* @param array $options Array of options to modify
*/
private function removeHeader($name, array &$options)
{
foreach (array_keys($options['_headers']) as $key) {
if (!strcasecmp($key, $name)) {
unset($options['_headers'][$key]);
return;
}
}
}
}

View file

@ -0,0 +1,284 @@
<?php
namespace GuzzleHttp\Adapter\Curl;
use GuzzleHttp\Adapter\AdapterInterface;
use GuzzleHttp\Adapter\ParallelAdapterInterface;
use GuzzleHttp\Adapter\TransactionInterface;
use GuzzleHttp\Event\RequestEvents;
use GuzzleHttp\Exception\AdapterException;
use GuzzleHttp\Exception\RequestException;
use GuzzleHttp\Message\MessageFactoryInterface;
/**
* HTTP adapter that uses cURL multi as a transport layer
*
* When using the CurlAdapter, custom curl options can be specified as an
* associative array of curl option constants mapping to values in the
* **curl** key of a request's configuration options.
*
* In addition to being able to supply configuration options via the curl
* request config, you can also specify the select_timeout variable using the
* `GUZZLE_CURL_SELECT_TIMEOUT` environment variable.
*/
class MultiAdapter implements AdapterInterface, ParallelAdapterInterface
{
const ERROR_STR = 'See http://curl.haxx.se/libcurl/c/libcurl-errors.html for an explanation of cURL errors';
const ENV_SELECT_TIMEOUT = 'GUZZLE_CURL_SELECT_TIMEOUT';
/** @var CurlFactory */
private $curlFactory;
/** @var MessageFactoryInterface */
private $messageFactory;
/** @var array Array of curl multi handles */
private $multiHandles = [];
/** @var array Array of curl multi handles */
private $multiOwned = [];
/** @var double */
private $selectTimeout;
/**
* Accepts an associative array of options:
*
* - handle_factory: Optional callable factory used to create cURL handles.
* The callable is invoked with the following arguments:
* TransactionInterface, MessageFactoryInterface, and an optional cURL
* handle to modify. The factory method must then return a cURL resource.
* - select_timeout: Specify a float in seconds to use for a
* curl_multi_select timeout.
*
* @param MessageFactoryInterface $messageFactory
* @param array $options Array of options to use with the adapter:
*/
public function __construct(
MessageFactoryInterface $messageFactory,
array $options = []
) {
$this->messageFactory = $messageFactory;
$this->curlFactory = isset($options['handle_factory'])
? $options['handle_factory']
: new CurlFactory();
if (isset($options['select_timeout'])) {
$this->selectTimeout = $options['select_timeout'];
} elseif (isset($_SERVER[self::ENV_SELECT_TIMEOUT])) {
$this->selectTimeout = $_SERVER[self::ENV_SELECT_TIMEOUT];
} else {
$this->selectTimeout = 1;
}
}
public function __destruct()
{
foreach ($this->multiHandles as $handle) {
if (is_resource($handle)) {
curl_multi_close($handle);
}
}
}
/**
* Throw an exception for a cURL multi response
*
* @param int $code Curl response code
* @throws AdapterException
*/
public static function throwMultiError($code)
{
$buffer = function_exists('curl_multi_strerror')
? curl_multi_strerror($code)
: self::ERROR_STR;
throw new AdapterException(sprintf('cURL error %s: %s', $code, $buffer));
}
public function send(TransactionInterface $transaction)
{
$context = new BatchContext($this->checkoutMultiHandle(), true);
$this->addHandle($transaction, $context);
$this->perform($context);
return $transaction->getResponse();
}
public function sendAll(\Iterator $transactions, $parallel)
{
$context = new BatchContext(
$this->checkoutMultiHandle(),
false,
$transactions
);
foreach (new \LimitIterator($transactions, 0, $parallel) as $trans) {
$this->addHandle($trans, $context);
}
$this->perform($context);
}
private function perform(BatchContext $context)
{
// The first curl_multi_select often times out no matter what, but is
// usually required for fast transfers.
$active = false;
$multi = $context->getMultiHandle();
do {
while (($mrc = curl_multi_exec($multi, $active)) == CURLM_CALL_MULTI_PERFORM);
if ($mrc != CURLM_OK && $mrc != CURLM_CALL_MULTI_PERFORM) {
self::throwMultiError($mrc);
}
// Need to check if there are pending transactions before processing
// them so that we don't bail from the loop too early.
$pending = $context->hasPending();
$this->processMessages($context);
if ($active && curl_multi_select($multi, $this->selectTimeout) === -1) {
// Perform a usleep if a select returns -1.
// See: https://bugs.php.net/bug.php?id=61141
usleep(250);
}
} while ($active || $pending);
$this->releaseMultiHandle($multi);
}
private function processMessages(BatchContext $context)
{
$multi = $context->getMultiHandle();
while ($done = curl_multi_info_read($multi)) {
$transaction = $context->findTransaction($done['handle']);
$this->processResponse($transaction, $done, $context);
// Add the next transaction if there are more in the queue
if ($next = $context->nextPending()) {
$this->addHandle($next, $context);
}
}
}
private function processResponse(
TransactionInterface $transaction,
array $curl,
BatchContext $context
) {
$info = $context->removeTransaction($transaction);
try {
if (!$this->isCurlException($transaction, $curl, $context, $info)) {
RequestEvents::emitComplete($transaction, $info);
}
} catch (RequestException $e) {
$this->throwException($e, $context);
}
}
private function addHandle(
TransactionInterface $transaction,
BatchContext $context
) {
try {
RequestEvents::emitBefore($transaction);
// Only transfer if the request was not intercepted
if (!$transaction->getResponse()) {
$factory = $this->curlFactory;
$context->addTransaction(
$transaction,
$factory($transaction, $this->messageFactory)
);
}
} catch (RequestException $e) {
$this->throwException($e, $context);
}
}
private function isCurlException(
TransactionInterface $transaction,
array $curl,
BatchContext $context,
array $info
) {
if (CURLM_OK == $curl['result'] ||
CURLM_CALL_MULTI_PERFORM == $curl['result']
) {
return false;
}
$request = $transaction->getRequest();
try {
// Send curl stats along if they are available
$stats = ['curl_result' => $curl['result']] + $info;
RequestEvents::emitError(
$transaction,
new RequestException(
sprintf(
'[curl] (#%s) %s [url] %s',
$curl['result'],
function_exists('curl_strerror')
? curl_strerror($curl['result'])
: self::ERROR_STR,
$request->getUrl()
),
$request
),
$stats
);
} catch (RequestException $e) {
$this->throwException($e, $context);
}
return true;
}
private function throwException(RequestException $e, BatchContext $context)
{
if ($context->throwsExceptions()) {
$this->releaseMultiHandle($context->getMultiHandle());
throw $e;
}
}
/**
* Returns a curl_multi handle from the cache or creates a new one
*
* @return resource
*/
private function checkoutMultiHandle()
{
// Find an unused handle in the cache
$key = array_search(false, $this->multiOwned, true);
if (false !== $key) {
$this->multiOwned[$key] = true;
return $this->multiHandles[$key];
}
// Add a new handle
$handle = curl_multi_init();
$id = (int) $handle;
$this->multiHandles[$id] = $handle;
$this->multiOwned[$id] = true;
return $handle;
}
/**
* Releases a curl_multi handle back into the cache and removes excess cache
*
* @param resource $handle Curl multi handle to remove
*/
private function releaseMultiHandle($handle)
{
$id = (int) $handle;
if (count($this->multiHandles) <= 3) {
$this->multiOwned[$id] = false;
} else {
// Prune excessive handles
curl_multi_close($this->multiHandles[$id]);
unset($this->multiHandles[$id], $this->multiOwned[$id]);
}
}
}

View file

@ -0,0 +1,130 @@
<?php
namespace GuzzleHttp\Adapter\Curl;
use GuzzleHttp\Adapter\TransactionInterface;
use GuzzleHttp\Event\RequestEvents;
use GuzzleHttp\Message\MessageFactoryInterface;
use GuzzleHttp\Stream\Stream;
use GuzzleHttp\Stream\StreamInterface;
/**
* Mediator between curl handles and request objects
*/
class RequestMediator
{
/** @var TransactionInterface */
private $transaction;
/** @var MessageFactoryInterface */
private $messageFactory;
private $statusCode;
private $reasonPhrase;
private $body;
private $protocolVersion;
private $headers;
/**
* @param TransactionInterface $transaction Transaction to populate
* @param MessageFactoryInterface $messageFactory Creates responses
*/
public function __construct(
TransactionInterface $transaction,
MessageFactoryInterface $messageFactory
) {
$this->transaction = $transaction;
$this->messageFactory = $messageFactory;
}
/**
* Set the body that will hold the response body
*
* @param StreamInterface $body Response body
*/
public function setResponseBody(StreamInterface $body = null)
{
$this->body = $body;
}
/**
* Receive a response header from curl
*
* @param resource $curl Curl handle
* @param string $header Received header
*
* @return int
*/
public function receiveResponseHeader($curl, $header)
{
static $normalize = ["\r", "\n"];
$length = strlen($header);
$header = str_replace($normalize, '', $header);
if (strpos($header, 'HTTP/') === 0) {
$startLine = explode(' ', $header, 3);
// Only download the body to a target body when a successful
// response is received.
if ($startLine[1][0] != '2') {
$this->body = null;
}
$this->statusCode = $startLine[1];
$this->reasonPhrase = isset($startLine[2]) ? $startLine[2] : null;
$this->protocolVersion = substr($startLine[0], -3);
$this->headers = [];
} elseif ($pos = strpos($header, ':')) {
$this->headers[substr($header, 0, $pos)][] = substr($header, $pos + 1);
} elseif ($header == '' && $this->statusCode >= 200) {
$response = $this->messageFactory->createResponse(
$this->statusCode,
$this->headers,
$this->body,
[
'protocol_version' => $this->protocolVersion,
'reason_phrase' => $this->reasonPhrase
]
);
$this->headers = $this->body = null;
$this->transaction->setResponse($response);
// Allows events to react before downloading any of the body
RequestEvents::emitHeaders($this->transaction);
}
return $length;
}
/**
* Write data to the response body of a request
*
* @param resource $curl
* @param string $write
*
* @return int
*/
public function writeResponseBody($curl, $write)
{
if (!($response = $this->transaction->getResponse())) {
return 0;
}
// Add a default body on the response if one was not found
if (!($body = $response->getBody())) {
$body = new Stream(fopen('php://temp', 'r+'));
$response->setBody($body);
}
return $body->write($write);
}
/**
* Read data from the request body and send it to curl
*
* @param resource $ch Curl handle
* @param resource $fd File descriptor
* @param int $length Amount of data to read
*
* @return string
*/
public function readRequestBody($ch, $fd, $length)
{
return (string) $this->transaction->getRequest()->getBody()->read($length);
}
}

View file

@ -0,0 +1,34 @@
<?php
namespace GuzzleHttp\Adapter;
use GuzzleHttp\Exception\RequestException;
/**
* Decorates a regular AdapterInterface object and creates a
* ParallelAdapterInterface object that sends multiple HTTP requests serially.
*/
class FakeParallelAdapter implements ParallelAdapterInterface
{
/** @var AdapterInterface */
private $adapter;
/**
* @param AdapterInterface $adapter Adapter used to send requests
*/
public function __construct(AdapterInterface $adapter)
{
$this->adapter = $adapter;
}
public function sendAll(\Iterator $transactions, $parallel)
{
foreach ($transactions as $transaction) {
try {
$this->adapter->send($transaction);
} catch (RequestException $e) {
// no op for batch transaction
}
}
}
}

View file

@ -0,0 +1,60 @@
<?php
namespace GuzzleHttp\Adapter;
use GuzzleHttp\Event\RequestEvents;
use GuzzleHttp\Message\ResponseInterface;
/**
* Adapter that can be used to associate mock responses with a transaction
* while still emulating the event workflow of real adapters.
*/
class MockAdapter implements AdapterInterface
{
private $response;
/**
* @param ResponseInterface|callable $response Response to serve or function
* to invoke that handles a transaction
*/
public function __construct($response = null)
{
$this->setResponse($response);
}
/**
* Set the response that will be served by the adapter
*
* @param ResponseInterface|callable $response Response to serve or
* function to invoke that handles a transaction
*/
public function setResponse($response)
{
$this->response = $response;
}
public function send(TransactionInterface $transaction)
{
RequestEvents::emitBefore($transaction);
if (!$transaction->getResponse()) {
// Read the request body if it is present
if ($transaction->getRequest()->getBody()) {
$transaction->getRequest()->getBody()->__toString();
}
$response = is_callable($this->response)
? call_user_func($this->response, $transaction)
: $this->response;
if (!$response instanceof ResponseInterface) {
throw new \RuntimeException('Invalid mocked response');
}
$transaction->setResponse($response);
RequestEvents::emitHeaders($transaction);
RequestEvents::emitComplete($transaction);
}
return $transaction->getResponse();
}
}

View file

@ -0,0 +1,23 @@
<?php
namespace GuzzleHttp\Adapter;
/**
* Adapter interface used to transfer multiple HTTP requests in parallel.
*
* Parallel adapters follow the same rules as AdapterInterface except that
* RequestExceptions are never thrown in a parallel transfer and parallel
* adapters do not return responses.
*/
interface ParallelAdapterInterface
{
/**
* Transfers multiple HTTP requests in parallel.
*
* RequestExceptions MUST not be thrown from a parallel transfer.
*
* @param \Iterator $transactions Iterator that yields TransactionInterface
* @param int $parallel Max number of requests to send in parallel
*/
public function sendAll(\Iterator $transactions, $parallel);
}

View file

@ -0,0 +1,347 @@
<?php
namespace GuzzleHttp\Adapter;
use GuzzleHttp\Event\RequestEvents;
use GuzzleHttp\Exception\AdapterException;
use GuzzleHttp\Exception\RequestException;
use GuzzleHttp\Message\MessageFactoryInterface;
use GuzzleHttp\Message\RequestInterface;
use GuzzleHttp\Stream;
/**
* HTTP adapter that uses PHP's HTTP stream wrapper.
*
* When using the StreamAdapter, custom stream context options can be specified
* using the **stream_context** option in a request's **config** option. The
* structure of the "stream_context" option is an associative array where each
* key is a transport name and each option is an associative array of options.
*/
class StreamAdapter implements AdapterInterface
{
/** @var MessageFactoryInterface */
private $messageFactory;
/**
* @param MessageFactoryInterface $messageFactory
*/
public function __construct(MessageFactoryInterface $messageFactory)
{
$this->messageFactory = $messageFactory;
}
public function send(TransactionInterface $transaction)
{
// HTTP/1.1 streams using the PHP stream wrapper require a
// Connection: close header. Setting here so that it is added before
// emitting the request.before_send event.
$request = $transaction->getRequest();
if ($request->getProtocolVersion() == '1.1' &&
!$request->hasHeader('Connection')
) {
$transaction->getRequest()->setHeader('Connection', 'close');
}
RequestEvents::emitBefore($transaction);
if (!$transaction->getResponse()) {
$this->createResponse($transaction);
RequestEvents::emitComplete($transaction);
}
return $transaction->getResponse();
}
private function createResponse(TransactionInterface $transaction)
{
$request = $transaction->getRequest();
$stream = $this->createStream($request, $http_response_header);
if (!$request->getConfig()['stream']) {
$stream = $this->getSaveToBody($request, $stream);
}
// Track the response headers of the request
$this->createResponseObject($http_response_header, $transaction, $stream);
}
/**
* Drain the steam into the destination stream
*/
private function getSaveToBody(RequestInterface $request, $stream)
{
if ($saveTo = $request->getConfig()['save_to']) {
// Stream the response into the destination stream
$saveTo = is_string($saveTo)
? Stream\create(fopen($saveTo, 'r+'))
: Stream\create($saveTo);
} else {
// Stream into the default temp stream
$saveTo = Stream\create();
}
while (!feof($stream)) {
$saveTo->write(fread($stream, 8096));
}
fclose($stream);
$saveTo->seek(0);
return $saveTo;
}
private function createResponseObject(
array $headers,
TransactionInterface $transaction,
$stream
) {
$parts = explode(' ', array_shift($headers), 3);
$options = ['protocol_version' => substr($parts[0], -3)];
if (isset($parts[2])) {
$options['reason_phrase'] = $parts[2];
}
// Set the size on the stream if it was returned in the response
$responseHeaders = [];
foreach ($headers as $header) {
$headerParts = explode(':', $header, 2);
$responseHeaders[$headerParts[0]] = isset($headerParts[1])
? $headerParts[1]
: '';
}
$response = $this->messageFactory->createResponse(
$parts[1],
$responseHeaders,
$stream,
$options
);
$transaction->setResponse($response);
RequestEvents::emitHeaders($transaction);
return $response;
}
/**
* Create a resource and check to ensure it was created successfully
*
* @param callable $callback Callable that returns stream resource
* @param RequestInterface $request Request used when throwing exceptions
* @param array $options Options used when throwing exceptions
*
* @return resource
* @throws RequestException on error
*/
private function createResource(callable $callback, RequestInterface $request, $options)
{
// Turn off error reporting while we try to initiate the request
$level = error_reporting(0);
$resource = call_user_func($callback);
error_reporting($level);
// If the resource could not be created, then grab the last error and
// throw an exception.
if (!is_resource($resource)) {
$message = 'Error creating resource. [url] ' . $request->getUrl() . ' ';
if (isset($options['http']['proxy'])) {
$message .= "[proxy] {$options['http']['proxy']} ";
}
foreach (error_get_last() as $key => $value) {
$message .= "[{$key}] {$value} ";
}
throw new RequestException(trim($message), $request);
}
return $resource;
}
/**
* Create the stream for the request with the context options.
*
* @param RequestInterface $request Request being sent
* @param mixed $http_response_header Populated by stream wrapper
*
* @return resource
*/
private function createStream(
RequestInterface $request,
&$http_response_header
) {
static $methods;
if (!$methods) {
$methods = array_flip(get_class_methods(__CLASS__));
}
$params = [];
$options = $this->getDefaultOptions($request);
foreach ($request->getConfig()->toArray() as $key => $value) {
$method = "add_{$key}";
if (isset($methods[$method])) {
$this->{$method}($request, $options, $value, $params);
}
}
$this->applyCustomOptions($request, $options);
$context = $this->createStreamContext($request, $options, $params);
return $this->createStreamResource(
$request,
$options,
$context,
$http_response_header
);
}
private function getDefaultOptions(RequestInterface $request)
{
$headers = '';
foreach ($request->getHeaders() as $name => $values) {
$headers .= $name . ': ' . implode(', ', $values) . "\r\n";
}
return [
'http' => [
'method' => $request->getMethod(),
'header' => trim($headers),
'protocol_version' => $request->getProtocolVersion(),
'ignore_errors' => true,
'follow_location' => 0,
'content' => (string) $request->getBody()
]
];
}
private function add_proxy(RequestInterface $request, &$options, $value, &$params)
{
if (!is_array($value)) {
$options['http']['proxy'] = $value;
$options['http']['request_fulluri'] = true;
} else {
$scheme = $request->getScheme();
if (isset($value[$scheme])) {
$options['http']['proxy'] = $value[$scheme];
$options['http']['request_fulluri'] = true;
}
}
}
private function add_timeout(RequestInterface $request, &$options, $value, &$params)
{
$options['http']['timeout'] = $value;
}
private function add_verify(RequestInterface $request, &$options, $value, &$params)
{
if ($value === true || is_string($value)) {
$options['http']['verify_peer'] = true;
if ($value !== true) {
if (!file_exists($value)) {
throw new \RuntimeException("SSL certificate authority file not found: {$value}");
}
$options['http']['allow_self_signed'] = true;
$options['http']['cafile'] = $value;
}
} elseif ($value === false) {
$options['http']['verify_peer'] = false;
}
}
private function add_cert(RequestInterface $request, &$options, $value, &$params)
{
if (is_array($value)) {
$options['http']['passphrase'] = $value[1];
$value = $value[0];
}
if (!file_exists($value)) {
throw new \RuntimeException("SSL certificate not found: {$value}");
}
$options['http']['local_cert'] = $value;
}
private function add_debug(RequestInterface $request, &$options, $value, &$params)
{
static $map = [
STREAM_NOTIFY_CONNECT => 'CONNECT',
STREAM_NOTIFY_AUTH_REQUIRED => 'AUTH_REQUIRED',
STREAM_NOTIFY_AUTH_RESULT => 'AUTH_RESULT',
STREAM_NOTIFY_MIME_TYPE_IS => 'MIME_TYPE_IS',
STREAM_NOTIFY_FILE_SIZE_IS => 'FILE_SIZE_IS',
STREAM_NOTIFY_REDIRECTED => 'REDIRECTED',
STREAM_NOTIFY_PROGRESS => 'PROGRESS',
STREAM_NOTIFY_FAILURE => 'FAILURE',
STREAM_NOTIFY_COMPLETED => 'COMPLETED',
STREAM_NOTIFY_RESOLVE => 'RESOLVE'
];
static $args = ['severity', 'message', 'message_code',
'bytes_transferred', 'bytes_max'];
if (!is_resource($value)) {
$value = fopen('php://output', 'w');
}
$params['notification'] = function () use ($request, $value, $map, $args) {
$passed = func_get_args();
$code = array_shift($passed);
fprintf($value, '<%s> [%s] ', $request->getUrl(), $map[$code]);
foreach (array_filter($passed) as $i => $v) {
fwrite($value, $args[$i] . ': "' . $v . '" ');
}
fwrite($value, "\n");
};
}
private function applyCustomOptions(
RequestInterface $request,
array &$options
) {
// Overwrite any generated options with custom options
if ($custom = $request->getConfig()['stream_context']) {
if (!is_array($custom)) {
throw new AdapterException('stream_context must be an array');
}
$options = array_replace_recursive($options, $custom);
}
}
private function createStreamContext(
RequestInterface $request,
array $options,
array $params
) {
return $this->createResource(function () use (
$request,
$options,
$params
) {
return stream_context_create($options, $params);
}, $request, $options);
}
private function createStreamResource(
RequestInterface $request,
array $options,
$context,
&$http_response_header
) {
$url = $request->getUrl();
// Add automatic gzip decompression
if (strpos($request->getHeader('Accept-Encoding'), 'gzip') !== false) {
$url = 'compress.zlib://' . $url;
}
return $this->createResource(function () use (
$url,
&$http_response_header,
$context
) {
if (false === strpos($url, 'http')) {
trigger_error("URL is invalid: {$url}", E_USER_WARNING);
return null;
}
return fopen($url, 'r', null, $context);
}, $request, $options);
}
}

View file

@ -0,0 +1,36 @@
<?php
namespace GuzzleHttp\Adapter;
/**
* Sends streaming requests to a streaming compatible adapter while sending all
* other requests to a default adapter.
*
* This, for example, could be useful for taking advantage of the performance
* benefits of the CurlAdapter while still supporting true streaming through
* the StreamAdapter.
*/
class StreamingProxyAdapter implements AdapterInterface
{
private $defaultAdapter;
private $streamingAdapter;
/**
* @param AdapterInterface $defaultAdapter Adapter used for non-streaming responses
* @param AdapterInterface $streamingAdapter Adapter used for streaming responses
*/
public function __construct(
AdapterInterface $defaultAdapter,
AdapterInterface $streamingAdapter
) {
$this->defaultAdapter = $defaultAdapter;
$this->streamingAdapter = $streamingAdapter;
}
public function send(TransactionInterface $transaction)
{
return $transaction->getRequest()->getConfig()['stream']
? $this->streamingAdapter->send($transaction)
: $this->defaultAdapter->send($transaction);
}
}

View file

@ -0,0 +1,49 @@
<?php
namespace GuzzleHttp\Adapter;
use GuzzleHttp\ClientInterface;
use GuzzleHttp\Message\RequestInterface;
use GuzzleHttp\Message\ResponseInterface;
class Transaction implements TransactionInterface
{
/** @var ClientInterface */
private $client;
/** @var RequestInterface */
private $request;
/** @var ResponseInterface */
private $response;
/**
* @param ClientInterface $client Client that is used to send the requests
* @param RequestInterface $request
*/
public function __construct(
ClientInterface $client,
RequestInterface $request
) {
$this->client = $client;
$this->request = $request;
}
public function getRequest()
{
return $this->request;
}
public function getResponse()
{
return $this->response;
}
public function setResponse(ResponseInterface $response)
{
$this->response = $response;
}
public function getClient()
{
return $this->client;
}
}

View file

@ -0,0 +1,35 @@
<?php
namespace GuzzleHttp\Adapter;
use GuzzleHttp\ClientInterface;
use GuzzleHttp\Message\RequestInterface;
use GuzzleHttp\Message\ResponseInterface;
/**
* Represents a transactions that consists of a request, response, and client
*/
interface TransactionInterface
{
/**
* @return RequestInterface
*/
public function getRequest();
/**
* @return ResponseInterface|null
*/
public function getResponse();
/**
* Set a response on the transaction
*
* @param ResponseInterface $response Response to set
*/
public function setResponse(ResponseInterface $response);
/**
* @return ClientInterface
*/
public function getClient();
}

View file

@ -0,0 +1,73 @@
<?php
namespace GuzzleHttp\Adapter;
use GuzzleHttp\ClientInterface;
use GuzzleHttp\Event\ListenerAttacherTrait;
use GuzzleHttp\Message\RequestInterface;
/**
* Converts a sequence of request objects into a transaction.
* @internal
*/
class TransactionIterator implements \Iterator
{
use ListenerAttacherTrait;
/** @var \Iterator */
private $source;
/** @var ClientInterface */
private $client;
/** @var array Listeners to attach to each request */
private $eventListeners = [];
public function __construct(
$source,
ClientInterface $client,
array $options
) {
$this->client = $client;
$this->eventListeners = $this->prepareListeners(
$options,
['before', 'complete', 'error']
);
if ($source instanceof \Iterator) {
$this->source = $source;
} elseif (is_array($source)) {
$this->source = new \ArrayIterator($source);
} else {
throw new \InvalidArgumentException('Expected an Iterator or array');
}
}
public function current()
{
$request = $this->source->current();
if (!$request instanceof RequestInterface) {
throw new \RuntimeException('All must implement RequestInterface');
}
$this->attachListeners($request, $this->eventListeners);
return new Transaction($this->client, $request);
}
public function next()
{
$this->source->next();
}
public function key()
{
return $this->source->key();
}
public function valid()
{
return $this->source->valid();
}
public function rewind() {}
}

View file

@ -0,0 +1,364 @@
<?php
namespace GuzzleHttp;
use GuzzleHttp\Adapter\Curl\MultiAdapter;
use GuzzleHttp\Event\HasEmitterTrait;
use GuzzleHttp\Adapter\FakeParallelAdapter;
use GuzzleHttp\Adapter\ParallelAdapterInterface;
use GuzzleHttp\Adapter\AdapterInterface;
use GuzzleHttp\Adapter\StreamAdapter;
use GuzzleHttp\Adapter\StreamingProxyAdapter;
use GuzzleHttp\Adapter\Curl\CurlAdapter;
use GuzzleHttp\Adapter\Transaction;
use GuzzleHttp\Adapter\TransactionIterator;
use GuzzleHttp\Exception\RequestException;
use GuzzleHttp\Message\MessageFactory;
use GuzzleHttp\Message\MessageFactoryInterface;
use GuzzleHttp\Message\RequestInterface;
/**
* HTTP client
*/
class Client implements ClientInterface
{
use HasEmitterTrait;
const DEFAULT_CONCURRENCY = 25;
/** @var MessageFactoryInterface Request factory used by the client */
private $messageFactory;
/** @var AdapterInterface */
private $adapter;
/** @var ParallelAdapterInterface */
private $parallelAdapter;
/** @var Url Base URL of the client */
private $baseUrl;
/** @var array Default request options */
private $defaults;
/**
* Clients accept an array of constructor parameters.
*
* Here's an example of creating a client using an URI template for the
* client's base_url and an array of default request options to apply
* to each request:
*
* $client = new Client([
* 'base_url' => [
* 'http://www.foo.com/{version}/',
* ['version' => '123']
* ],
* 'defaults' => [
* 'timeout' => 10,
* 'allow_redirects' => false,
* 'proxy' => '192.168.16.1:10'
* ]
* ]);
*
* @param array $config Client configuration settings
* - base_url: Base URL of the client that is merged into relative URLs.
* Can be a string or an array that contains a URI template followed
* by an associative array of expansion variables to inject into the
* URI template.
* - adapter: Adapter used to transfer requests
* - parallel_adapter: Adapter used to transfer requests in parallel
* - message_factory: Factory used to create request and response object
* - defaults: Default request options to apply to each request
* - emitter: Event emitter used for request events
*/
public function __construct(array $config = [])
{
$this->configureBaseUrl($config);
$this->configureDefaults($config);
$this->configureAdapter($config);
if (isset($config['emitter'])) {
$this->emitter = $config['emitter'];
}
}
/**
* Get the default User-Agent string to use with Guzzle
*
* @return string
*/
public static function getDefaultUserAgent()
{
static $defaultAgent = '';
if (!$defaultAgent) {
$defaultAgent = 'Guzzle/' . self::VERSION;
if (extension_loaded('curl')) {
$defaultAgent .= ' curl/' . curl_version()['version'];
}
$defaultAgent .= ' PHP/' . PHP_VERSION;
}
return $defaultAgent;
}
public function __call($name, $arguments)
{
return \GuzzleHttp\deprecation_proxy(
$this,
$name,
$arguments,
['getEventDispatcher' => 'getEmitter']
);
}
public function getDefaultOption($keyOrPath = null)
{
return $keyOrPath === null
? $this->defaults
: \GuzzleHttp\get_path($this->defaults, $keyOrPath);
}
public function setDefaultOption($keyOrPath, $value)
{
\GuzzleHttp\set_path($this->defaults, $keyOrPath, $value);
}
public function getBaseUrl()
{
return (string) $this->baseUrl;
}
public function createRequest($method, $url = null, array $options = [])
{
// Merge in default options
$options = array_replace_recursive($this->defaults, $options);
// Use a clone of the client's emitter
$options['config']['emitter'] = clone $this->getEmitter();
$request = $this->messageFactory->createRequest(
$method,
$url ? (string) $this->buildUrl($url) : (string) $this->baseUrl,
$options
);
return $request;
}
public function get($url = null, $options = [])
{
return $this->send($this->createRequest('GET', $url, $options));
}
public function head($url = null, array $options = [])
{
return $this->send($this->createRequest('HEAD', $url, $options));
}
public function delete($url = null, array $options = [])
{
return $this->send($this->createRequest('DELETE', $url, $options));
}
public function put($url = null, array $options = [])
{
return $this->send($this->createRequest('PUT', $url, $options));
}
public function patch($url = null, array $options = [])
{
return $this->send($this->createRequest('PATCH', $url, $options));
}
public function post($url = null, array $options = [])
{
return $this->send($this->createRequest('POST', $url, $options));
}
public function options($url = null, array $options = [])
{
return $this->send($this->createRequest('OPTIONS', $url, $options));
}
public function send(RequestInterface $request)
{
$transaction = new Transaction($this, $request);
try {
if ($response = $this->adapter->send($transaction)) {
return $response;
}
throw new \LogicException('No response was associated with the transaction');
} catch (RequestException $e) {
throw $e;
} catch (\Exception $e) {
// Wrap exceptions in a RequestException to adhere to the interface
throw new RequestException($e->getMessage(), $request, null, $e);
}
}
public function sendAll($requests, array $options = [])
{
if (!($requests instanceof TransactionIterator)) {
$requests = new TransactionIterator($requests, $this, $options);
}
$this->parallelAdapter->sendAll(
$requests,
isset($options['parallel'])
? $options['parallel']
: self::DEFAULT_CONCURRENCY
);
}
/**
* Get an array of default options to apply to the client
*
* @return array
*/
protected function getDefaultOptions()
{
$settings = [
'allow_redirects' => true,
'exceptions' => true,
'verify' => __DIR__ . '/cacert.pem'
];
// Use the bundled cacert if it is a regular file, or set to true if
// using a phar file (because curL and the stream wrapper can't read
// cacerts from the phar stream wrapper). Favor the ini setting over
// the system's cacert.
if (substr(__FILE__, 0, 7) == 'phar://') {
$settings['verify'] = ini_get('openssl.cafile') ?: true;
}
// Use the standard Linux HTTP_PROXY and HTTPS_PROXY if set
if (isset($_SERVER['HTTP_PROXY'])) {
$settings['proxy']['http'] = $_SERVER['HTTP_PROXY'];
}
if (isset($_SERVER['HTTPS_PROXY'])) {
$settings['proxy']['https'] = $_SERVER['HTTPS_PROXY'];
}
return $settings;
}
/**
* Expand a URI template and inherit from the base URL if it's relative
*
* @param string|array $url URL or URI template to expand
*
* @return string
*/
private function buildUrl($url)
{
if (!is_array($url)) {
if (strpos($url, '://')) {
return (string) $url;
}
return (string) $this->baseUrl->combine($url);
} elseif (strpos($url[0], '://')) {
return \GuzzleHttp\uri_template($url[0], $url[1]);
}
return (string) $this->baseUrl->combine(
\GuzzleHttp\uri_template($url[0], $url[1])
);
}
/**
* Get a default parallel adapter to use based on the environment
*
* @return ParallelAdapterInterface
*/
private function getDefaultParallelAdapter()
{
return extension_loaded('curl')
? new MultiAdapter($this->messageFactory)
: new FakeParallelAdapter($this->adapter);
}
/**
* Create a default adapter to use based on the environment
* @throws \RuntimeException
*/
private function getDefaultAdapter()
{
if (extension_loaded('curl')) {
$this->parallelAdapter = new MultiAdapter($this->messageFactory);
$this->adapter = function_exists('curl_reset')
? new CurlAdapter($this->messageFactory)
: $this->parallelAdapter;
if (ini_get('allow_url_fopen')) {
$this->adapter = new StreamingProxyAdapter(
$this->adapter,
new StreamAdapter($this->messageFactory)
);
}
} elseif (ini_get('allow_url_fopen')) {
$this->adapter = new StreamAdapter($this->messageFactory);
} else {
throw new \RuntimeException('Guzzle requires cURL, the '
. 'allow_url_fopen ini setting, or a custom HTTP adapter.');
}
}
private function configureBaseUrl(&$config)
{
if (!isset($config['base_url'])) {
$this->baseUrl = new Url('', '');
} elseif (is_array($config['base_url'])) {
$this->baseUrl = Url::fromString(
\GuzzleHttp\uri_template(
$config['base_url'][0],
$config['base_url'][1]
)
);
$config['base_url'] = (string) $this->baseUrl;
} else {
$this->baseUrl = Url::fromString($config['base_url']);
}
}
private function configureDefaults($config)
{
if (!isset($config['defaults'])) {
$this->defaults = $this->getDefaultOptions();
} else {
$this->defaults = array_replace(
$this->getDefaultOptions(),
$config['defaults']
);
}
// Add the default user-agent header
if (!isset($this->defaults['headers'])) {
$this->defaults['headers'] = [
'User-Agent' => static::getDefaultUserAgent()
];
} elseif (!isset(array_change_key_case($this->defaults['headers'])['user-agent'])) {
// Add the User-Agent header if one was not already set
$this->defaults['headers']['User-Agent'] = static::getDefaultUserAgent();
}
}
private function configureAdapter(&$config)
{
if (isset($config['message_factory'])) {
$this->messageFactory = $config['message_factory'];
} else {
$this->messageFactory = new MessageFactory();
}
if (isset($config['adapter'])) {
$this->adapter = $config['adapter'];
} else {
$this->getDefaultAdapter();
}
// If no parallel adapter was explicitly provided and one was not
// defaulted when creating the default adapter, then create one now.
if (isset($config['parallel_adapter'])) {
$this->parallelAdapter = $config['parallel_adapter'];
} elseif (!$this->parallelAdapter) {
$this->parallelAdapter = $this->getDefaultParallelAdapter();
}
}
}

View file

@ -0,0 +1,179 @@
<?php
namespace GuzzleHttp;
use GuzzleHttp\Event\HasEmitterInterface;
use GuzzleHttp\Message\RequestInterface;
use GuzzleHttp\Message\ResponseInterface;
use GuzzleHttp\Exception\RequestException;
use GuzzleHttp\Exception\AdapterException;
/**
* Client interface for sending HTTP requests
*/
interface ClientInterface extends HasEmitterInterface
{
const VERSION = '4.1.2';
/**
* Create and return a new {@see RequestInterface} object.
*
* Use an absolute path to override the base path of the client, or a
* relative path to append to the base path of the client. The URL can
* contain the query string as well. Use an array to provide a URL
* template and additional variables to use in the URL template expansion.
*
* @param string $method HTTP method
* @param string|array|Url $url URL or URI template
* @param array $options Array of request options to apply.
*
* @return RequestInterface
*/
public function createRequest($method, $url = null, array $options = []);
/**
* Send a GET request
*
* @param string|array|Url $url URL or URI template
* @param array $options Array of request options to apply.
*
* @return ResponseInterface
* @throws RequestException When an error is encountered
*/
public function get($url = null, $options = []);
/**
* Send a HEAD request
*
* @param string|array|Url $url URL or URI template
* @param array $options Array of request options to apply.
*
* @return ResponseInterface
* @throws RequestException When an error is encountered
*/
public function head($url = null, array $options = []);
/**
* Send a DELETE request
*
* @param string|array|Url $url URL or URI template
* @param array $options Array of request options to apply.
*
* @return ResponseInterface
* @throws RequestException When an error is encountered
*/
public function delete($url = null, array $options = []);
/**
* Send a PUT request
*
* @param string|array|Url $url URL or URI template
* @param array $options Array of request options to apply.
*
* @return ResponseInterface
* @throws RequestException When an error is encountered
*/
public function put($url = null, array $options = []);
/**
* Send a PATCH request
*
* @param string|array|Url $url URL or URI template
* @param array $options Array of request options to apply.
*
* @return ResponseInterface
* @throws RequestException When an error is encountered
*/
public function patch($url = null, array $options = []);
/**
* Send a POST request
*
* @param string|array|Url $url URL or URI template
* @param array $options Array of request options to apply.
*
* @return ResponseInterface
* @throws RequestException When an error is encountered
*/
public function post($url = null, array $options = []);
/**
* Send an OPTIONS request
*
* @param string|array|Url $url URL or URI template
* @param array $options Array of request options to apply.
*
* @return ResponseInterface
* @throws RequestException When an error is encountered
*/
public function options($url = null, array $options = []);
/**
* Sends a single request
*
* @param RequestInterface $request Request to send
*
* @return \GuzzleHttp\Message\ResponseInterface
* @throws \LogicException When the adapter does not populate a response
* @throws RequestException When an error is encountered
*/
public function send(RequestInterface $request);
/**
* Sends multiple requests in parallel.
*
* Exceptions are not thrown for failed requests. Callers are expected to
* register an "error" option to handle request errors OR directly register
* an event handler for the "error" event of a request's
* event emitter.
*
* The option values for 'before', 'after', and 'error' can be a callable,
* an associative array containing event data, or an array of event data
* arrays. Event data arrays contain the following keys:
*
* - fn: callable to invoke that receives the event
* - priority: Optional event priority (defaults to 0)
* - once: Set to true so that the event is removed after it is triggered
*
* @param array|\Iterator $requests Requests to send in parallel
* @param array $options Associative array of options
* - parallel: (int) Maximum number of requests to send in parallel
* - before: (callable|array) Receives a BeforeEvent
* - after: (callable|array) Receives a CompleteEvent
* - error: (callable|array) Receives a ErrorEvent
*
* @throws AdapterException When an error occurs in the HTTP adapter.
*/
public function sendAll($requests, array $options = []);
/**
* Get default request options of the client.
*
* @param string|null $keyOrPath The Path to a particular default request
* option to retrieve or pass null to retrieve all default request
* options. The syntax uses "/" to denote a path through nested PHP
* arrays. For example, "headers/content-type".
*
* @return mixed
*/
public function getDefaultOption($keyOrPath = null);
/**
* Set a default request option on the client so that any request created
* by the client will use the provided default value unless overridden
* explicitly when creating a request.
*
* @param string|null $keyOrPath The Path to a particular configuration
* value to set. The syntax uses a path notation that allows you to
* specify nested configuration values (e.g., 'headers/content-type').
* @param mixed $value Default request option value to set
*/
public function setDefaultOption($keyOrPath, $value);
/**
* Get the base URL of the client.
*
* @return string Returns the base URL if present
*/
public function getBaseUrl();
}

View file

@ -0,0 +1,265 @@
<?php
namespace GuzzleHttp;
/**
* Key value pair collection object
*/
class Collection implements
\ArrayAccess,
\IteratorAggregate,
\Countable,
ToArrayInterface
{
use HasDataTrait;
/**
* @param array $data Associative array of data to set
*/
public function __construct(array $data = [])
{
$this->data = $data;
}
/**
* Create a new collection from an array, validate the keys, and add default
* values where missing
*
* @param array $config Configuration values to apply.
* @param array $defaults Default parameters
* @param array $required Required parameter names
*
* @return self
* @throws \InvalidArgumentException if a parameter is missing
*/
public static function fromConfig(
array $config = [],
array $defaults = [],
array $required = []
) {
$data = $config + $defaults;
if ($missing = array_diff($required, array_keys($data))) {
throw new \InvalidArgumentException(
'Config is missing the following keys: ' .
implode(', ', $missing));
}
return new self($data);
}
/**
* Removes all key value pairs
*
* @return Collection
*/
public function clear()
{
$this->data = [];
return $this;
}
/**
* Get a specific key value.
*
* @param string $key Key to retrieve.
*
* @return mixed|null Value of the key or NULL
*/
public function get($key)
{
return isset($this->data[$key]) ? $this->data[$key] : null;
}
/**
* Set a key value pair
*
* @param string $key Key to set
* @param mixed $value Value to set
*
* @return Collection Returns a reference to the object
*/
public function set($key, $value)
{
$this->data[$key] = $value;
return $this;
}
/**
* Add a value to a key. If a key of the same name has already been added,
* the key value will be converted into an array and the new value will be
* pushed to the end of the array.
*
* @param string $key Key to add
* @param mixed $value Value to add to the key
*
* @return Collection Returns a reference to the object.
*/
public function add($key, $value)
{
if (!array_key_exists($key, $this->data)) {
$this->data[$key] = $value;
} elseif (is_array($this->data[$key])) {
$this->data[$key][] = $value;
} else {
$this->data[$key] = array($this->data[$key], $value);
}
return $this;
}
/**
* Remove a specific key value pair
*
* @param string $key A key to remove
*
* @return Collection
*/
public function remove($key)
{
unset($this->data[$key]);
return $this;
}
/**
* Get all keys in the collection
*
* @return array
*/
public function getKeys()
{
return array_keys($this->data);
}
/**
* Returns whether or not the specified key is present.
*
* @param string $key The key for which to check the existence.
*
* @return bool
*/
public function hasKey($key)
{
return array_key_exists($key, $this->data);
}
/**
* Checks if any keys contains a certain value
*
* @param string $value Value to search for
*
* @return mixed Returns the key if the value was found FALSE if the value
* was not found.
*/
public function hasValue($value)
{
return array_search($value, $this->data, true);
}
/**
* Replace the data of the object with the value of an array
*
* @param array $data Associative array of data
*
* @return Collection Returns a reference to the object
*/
public function replace(array $data)
{
$this->data = $data;
return $this;
}
/**
* Add and merge in a Collection or array of key value pair data.
*
* @param Collection|array $data Associative array of key value pair data
*
* @return Collection Returns a reference to the object.
*/
public function merge($data)
{
foreach ($data as $key => $value) {
$this->add($key, $value);
}
return $this;
}
/**
* Over write key value pairs in this collection with all of the data from
* an array or collection.
*
* @param array|\Traversable $data Values to override over this config
*
* @return self
*/
public function overwriteWith($data)
{
if (is_array($data)) {
$this->data = $data + $this->data;
} elseif ($data instanceof Collection) {
$this->data = $data->toArray() + $this->data;
} else {
foreach ($data as $key => $value) {
$this->data[$key] = $value;
}
}
return $this;
}
/**
* Returns a Collection containing all the elements of the collection after
* applying the callback function to each one.
*
* The callable should accept three arguments:
* - (string) $key
* - (string) $value
* - (array) $context
*
* The callable must return a the altered or unaltered value.
*
* @param callable $closure Map function to apply
* @param array $context Context to pass to the callable
*
* @return Collection
*/
public function map(callable $closure, array $context = [])
{
$collection = new static();
foreach ($this as $key => $value) {
$collection[$key] = $closure($key, $value, $context);
}
return $collection;
}
/**
* Iterates over each key value pair in the collection passing them to the
* callable. If the callable returns true, the current value from input is
* returned into the result Collection.
*
* The callable must accept two arguments:
* - (string) $key
* - (string) $value
*
* @param callable $closure Evaluation function
*
* @return Collection
*/
public function filter(callable $closure)
{
$collection = new static();
foreach ($this->data as $key => $value) {
if ($closure($key, $value)) {
$collection[$key] = $value;
}
}
return $collection;
}
}

View file

@ -0,0 +1,249 @@
<?php
namespace GuzzleHttp\Cookie;
use GuzzleHttp\Message\RequestInterface;
use GuzzleHttp\Message\ResponseInterface;
use GuzzleHttp\ToArrayInterface;
/**
* Cookie jar that stores cookies an an array
*/
class CookieJar implements CookieJarInterface, ToArrayInterface
{
/** @var SetCookie[] Loaded cookie data */
private $cookies = [];
/** @var bool */
private $strictMode;
/**
* @param bool $strictMode Set to true to throw exceptions when invalid
* cookies are added to the cookie jar.
* @param array $cookieArray Array of SetCookie objects or a hash of arrays
* that can be used with the SetCookie constructor
*/
public function __construct($strictMode = false, $cookieArray = [])
{
$this->strictMode = $strictMode;
foreach ($cookieArray as $cookie) {
if (!($cookieArray instanceof SetCookie)) {
$cookie = new SetCookie($cookie);
}
$this->setCookie($cookie);
}
}
/**
* Create a new Cookie jar from an associative array and domain.
*
* @param array $cookies Cookies to create the jar from
* @param string $domain Domain to set the cookies to
*
* @return self
*/
public static function fromArray(array $cookies, $domain)
{
$cookieJar = new self();
foreach ($cookies as $name => $value) {
$cookieJar->setCookie(new SetCookie([
'Domain' => $domain,
'Name' => $name,
'Value' => $value,
'Discard' => true
]));
}
return $cookieJar;
}
/**
* Quote the cookie value if it is not already quoted and it contains
* problematic characters.
*
* @param string $value Value that may or may not need to be quoted
*
* @return string
*/
public static function getCookieValue($value)
{
if (substr($value, 0, 1) !== '"' &&
substr($value, -1, 1) !== '"' &&
strpbrk($value, ';,')
) {
$value = '"' . $value . '"';
}
return $value;
}
public function toArray()
{
return array_map(function (SetCookie $cookie) {
return $cookie->toArray();
}, $this->getIterator()->getArrayCopy());
}
public function clear($domain = null, $path = null, $name = null)
{
if (!$domain) {
$this->cookies = [];
return;
} elseif (!$path) {
$this->cookies = array_filter(
$this->cookies,
function (SetCookie $cookie) use ($path, $domain) {
return !$cookie->matchesDomain($domain);
}
);
} elseif (!$name) {
$this->cookies = array_filter(
$this->cookies,
function (SetCookie $cookie) use ($path, $domain) {
return !($cookie->matchesPath($path) &&
$cookie->matchesDomain($domain));
}
);
} else {
$this->cookies = array_filter(
$this->cookies,
function (SetCookie $cookie) use ($path, $domain, $name) {
return !($cookie->getName() == $name &&
$cookie->matchesPath($path) &&
$cookie->matchesDomain($domain));
}
);
}
}
public function clearSessionCookies()
{
$this->cookies = array_filter(
$this->cookies,
function (SetCookie $cookie) {
return !$cookie->getDiscard() && $cookie->getExpires();
}
);
}
public function setCookie(SetCookie $cookie)
{
// Only allow cookies with set and valid domain, name, value
$result = $cookie->validate();
if ($result !== true) {
if ($this->strictMode) {
throw new \RuntimeException('Invalid cookie: ' . $result);
} else {
$this->removeCookieIfEmpty($cookie);
return false;
}
}
// Resolve conflicts with previously set cookies
foreach ($this->cookies as $i => $c) {
// Two cookies are identical, when their path, and domain are
// identical.
if ($c->getPath() != $cookie->getPath() ||
$c->getDomain() != $cookie->getDomain() ||
$c->getName() != $cookie->getName()
) {
continue;
}
// The previously set cookie is a discard cookie and this one is
// not so allow the new cookie to be set
if (!$cookie->getDiscard() && $c->getDiscard()) {
unset($this->cookies[$i]);
continue;
}
// If the new cookie's expiration is further into the future, then
// replace the old cookie
if ($cookie->getExpires() > $c->getExpires()) {
unset($this->cookies[$i]);
continue;
}
// If the value has changed, we better change it
if ($cookie->getValue() !== $c->getValue()) {
unset($this->cookies[$i]);
continue;
}
// The cookie exists, so no need to continue
return false;
}
$this->cookies[] = $cookie;
return true;
}
public function count()
{
return count($this->cookies);
}
public function getIterator()
{
return new \ArrayIterator(array_values($this->cookies));
}
public function extractCookies(
RequestInterface $request,
ResponseInterface $response
) {
if ($cookieHeader = $response->getHeader('Set-Cookie', true)) {
foreach ($cookieHeader as $cookie) {
$sc = SetCookie::fromString($cookie);
if (!$sc->getDomain()) {
$sc->setDomain($request->getHost());
}
$this->setCookie($sc);
}
}
}
public function addCookieHeader(RequestInterface $request)
{
$values = [];
$scheme = $request->getScheme();
$host = $request->getHost();
$path = $request->getPath();
foreach ($this->cookies as $cookie) {
if ($cookie->matchesPath($path) &&
$cookie->matchesDomain($host) &&
!$cookie->isExpired() &&
(!$cookie->getSecure() || $scheme == 'https')
) {
$values[] = $cookie->getName() . '='
. self::getCookieValue($cookie->getValue());
}
}
if ($values) {
$request->setHeader('Cookie', implode(';', $values));
}
}
/**
* If a cookie already exists and the server asks to set it again with a
* null value, the cookie must be deleted.
*
* @param SetCookie $cookie
*/
private function removeCookieIfEmpty(SetCookie $cookie)
{
$cookieValue = $cookie->getValue();
if ($cookieValue === null || $cookieValue === '') {
$this->clear(
$cookie->getDomain(),
$cookie->getPath(),
$cookie->getName()
);
}
}
}

View file

@ -0,0 +1,76 @@
<?php
namespace GuzzleHttp\Cookie;
use GuzzleHttp\Message\RequestInterface;
use GuzzleHttp\Message\ResponseInterface;
/**
* Stores HTTP cookies.
*
* It extracts cookies from HTTP requests, and returns them in HTTP responses.
* CookieJarInterface instances automatically expire contained cookies when
* necessary. Subclasses are also responsible for storing and retrieving
* cookies from a file, database, etc.
*
* @link http://docs.python.org/2/library/cookielib.html Inspiration
*/
interface CookieJarInterface extends \Countable, \IteratorAggregate
{
/**
* Add a Cookie header to a request.
*
* If no matching cookies are found in the cookie jar, then no Cookie
* header is added to the request.
*
* @param RequestInterface $request Request object to update
*/
public function addCookieHeader(RequestInterface $request);
/**
* Extract cookies from an HTTP response and store them in the CookieJar.
*
* @param RequestInterface $request Request that was sent
* @param ResponseInterface $response Response that was received
*/
public function extractCookies(
RequestInterface $request,
ResponseInterface $response
);
/**
* Sets a cookie in the cookie jar.
*
* @param SetCookie $cookie Cookie to set.
*
* @return bool Returns true on success or false on failure
*/
public function setCookie(SetCookie $cookie);
/**
* Remove cookies currently held in the cookie jar.
*
* Invoking this method without arguments will empty the whole cookie jar.
* If given a $domain argument only cookies belonging to that domain will
* be removed. If given a $domain and $path argument, cookies belonging to
* the specified path within that domain are removed. If given all three
* arguments, then the cookie with the specified name, path and domain is
* removed.
*
* @param string $domain Clears cookies matching a domain
* @param string $path Clears cookies matching a domain and path
* @param string $name Clears cookies matching a domain, path, and name
*
* @return CookieJarInterface
*/
public function clear($domain = null, $path = null, $name = null);
/**
* Discard all sessions cookies.
*
* Removes cookies that don't have an expire field or a have a discard
* field set to true. To be called when the user agent shuts down according
* to RFC 2965.
*/
public function clearSessionCookies();
}

View file

@ -0,0 +1,85 @@
<?php
namespace GuzzleHttp\Cookie;
/**
* Persists non-session cookies using a JSON formatted file
*/
class FileCookieJar extends CookieJar
{
/** @var string filename */
private $filename;
/**
* Create a new FileCookieJar object
*
* @param string $cookieFile File to store the cookie data
*
* @throws \RuntimeException if the file cannot be found or created
*/
public function __construct($cookieFile)
{
$this->filename = $cookieFile;
if (file_exists($cookieFile)) {
$this->load($cookieFile);
}
}
/**
* Saves the file when shutting down
*/
public function __destruct()
{
$this->save($this->filename);
}
/**
* Saves the cookies to a file.
*
* @param string $filename File to save
* @throws \RuntimeException if the file cannot be found or created
*/
public function save($filename)
{
$json = [];
foreach ($this as $cookie) {
if ($cookie->getExpires() && !$cookie->getDiscard()) {
$json[] = $cookie->toArray();
}
}
if (false === file_put_contents($filename, json_encode($json))) {
// @codeCoverageIgnoreStart
throw new \RuntimeException("Unable to save file {$filename}");
// @codeCoverageIgnoreEnd
}
}
/**
* Load cookies from a JSON formatted file.
*
* Old cookies are kept unless overwritten by newly loaded ones.
*
* @param string $filename Cookie file to load.
* @throws \RuntimeException if the file cannot be loaded.
*/
public function load($filename)
{
$json = file_get_contents($filename);
if (false === $json) {
// @codeCoverageIgnoreStart
throw new \RuntimeException("Unable to load file {$filename}");
// @codeCoverageIgnoreEnd
}
$data = \GuzzleHttp\json_decode($json, true);
if (is_array($data)) {
foreach (\GuzzleHttp\json_decode($json, true) as $cookie) {
$this->setCookie(new SetCookie($cookie));
}
} elseif (strlen($data)) {
throw new \RuntimeException("Invalid cookie file: {$filename}");
}
}
}

View file

@ -0,0 +1,65 @@
<?php
namespace GuzzleHttp\Cookie;
/**
* Persists cookies in the client session
*/
class SessionCookieJar extends CookieJar
{
/** @var string session key */
private $sessionKey;
/**
* Create a new SessionCookieJar object
*
* @param string $sessionKey Session key name to store the cookie data in session
*/
public function __construct($sessionKey)
{
$this->sessionKey = $sessionKey;
$this->load();
}
/**
* Saves cookies to session when shutting down
*/
public function __destruct()
{
$this->save();
}
/**
* Save cookies to the client session
*/
public function save()
{
$json = [];
foreach ($this as $cookie) {
if ($cookie->getExpires() && !$cookie->getDiscard()) {
$json[] = $cookie->toArray();
}
}
$_SESSION[$this->sessionKey] = json_encode($json);
}
/**
* Load the contents of the client session into the data array
*/
protected function load()
{
$cookieJar = isset($_SESSION[$this->sessionKey])
? $_SESSION[$this->sessionKey]
: null;
$data = \GuzzleHttp\json_decode($cookieJar, true);
if (is_array($data)) {
foreach ($data as $cookie) {
$this->setCookie(new SetCookie($cookie));
}
} elseif (strlen($data)) {
throw new \RuntimeException("Invalid cookie data");
}
}
}

View file

@ -0,0 +1,410 @@
<?php
namespace GuzzleHttp\Cookie;
use GuzzleHttp\ToArrayInterface;
/**
* Set-Cookie object
*/
class SetCookie implements ToArrayInterface
{
/** @var array */
private static $defaults = [
'Name' => null,
'Value' => null,
'Domain' => null,
'Path' => '/',
'Max-Age' => null,
'Expires' => null,
'Secure' => false,
'Discard' => false,
'HttpOnly' => false
];
/** @var array Cookie data */
private $data;
/**
* Create a new SetCookie object from a string
*
* @param string $cookie Set-Cookie header string
*
* @return self
*/
public static function fromString($cookie)
{
// Create the default return array
$data = self::$defaults;
// Explode the cookie string using a series of semicolons
$pieces = array_filter(array_map('trim', explode(';', $cookie)));
// The name of the cookie (first kvp) must include an equal sign.
if (empty($pieces) || !strpos($pieces[0], '=')) {
return new self($data);
}
// Add the cookie pieces into the parsed data array
foreach ($pieces as $part) {
$cookieParts = explode('=', $part, 2);
$key = trim($cookieParts[0]);
$value = isset($cookieParts[1])
? trim($cookieParts[1], " \n\r\t\0\x0B\"")
: true;
// Only check for non-cookies when cookies have been found
if (empty($data['Name'])) {
$data['Name'] = $key;
$data['Value'] = $value;
} else {
foreach (array_keys(self::$defaults) as $search) {
if (!strcasecmp($search, $key)) {
$data[$search] = $value;
continue 2;
}
}
$data[$key] = $value;
}
}
return new self($data);
}
/**
* @param array $data Array of cookie data provided by a Cookie parser
*/
public function __construct(array $data = [])
{
$this->data = array_replace(self::$defaults, $data);
// Extract the Expires value and turn it into a UNIX timestamp if needed
if (!$this->getExpires() && $this->getMaxAge()) {
// Calculate the Expires date
$this->setExpires(time() + $this->getMaxAge());
} elseif ($this->getExpires() && !is_numeric($this->getExpires())) {
$this->setExpires($this->getExpires());
}
}
public function __toString()
{
$str = $this->data['Name'] . '=' . $this->data['Value'] . '; ';
foreach ($this->data as $k => $v) {
if ($k != 'Name' && $k != 'Value'&& $v !== null && $v !== false) {
if ($k == 'Expires') {
$str .= 'Expires=' . gmdate('D, d M Y H:i:s \G\M\T', $v) . '; ';
} else {
$str .= ($v === true ? $k : "{$k}={$v}") . '; ';
}
}
}
return rtrim($str, '; ');
}
public function toArray()
{
return $this->data;
}
/**
* Get the cookie name
*
* @return string
*/
public function getName()
{
return $this->data['Name'];
}
/**
* Set the cookie name
*
* @param string $name Cookie name
*
* @return self
*/
public function setName($name)
{
$this->data['Name'] = $name;
return $this;
}
/**
* Get the cookie value
*
* @return string
*/
public function getValue()
{
return $this->data['Value'];
}
/**
* Set the cookie value
*
* @param string $value Cookie value
*
* @return self
*/
public function setValue($value)
{
$this->data['Value'] = $value;
return $this;
}
/**
* Get the domain
*
* @return string|null
*/
public function getDomain()
{
return $this->data['Domain'];
}
/**
* Set the domain of the cookie
*
* @param string $domain
*
* @return self
*/
public function setDomain($domain)
{
$this->data['Domain'] = $domain;
return $this;
}
/**
* Get the path
*
* @return string
*/
public function getPath()
{
return $this->data['Path'];
}
/**
* Set the path of the cookie
*
* @param string $path Path of the cookie
*
* @return self
*/
public function setPath($path)
{
$this->data['Path'] = $path;
return $this;
}
/**
* Maximum lifetime of the cookie in seconds
*
* @return int|null
*/
public function getMaxAge()
{
return $this->data['Max-Age'];
}
/**
* Set the max-age of the cookie
*
* @param int $maxAge Max age of the cookie in seconds
*
* @return self
*/
public function setMaxAge($maxAge)
{
$this->data['Max-Age'] = $maxAge;
return $this;
}
/**
* The UNIX timestamp when the cookie Expires
*
* @return mixed
*/
public function getExpires()
{
return $this->data['Expires'];
}
/**
* Set the unix timestamp for which the cookie will expire
*
* @param int $timestamp Unix timestamp
*
* @return self
*/
public function setExpires($timestamp)
{
$this->data['Expires'] = is_numeric($timestamp)
? (int) $timestamp
: strtotime($timestamp);
return $this;
}
/**
* Get whether or not this is a secure cookie
*
* @return null|bool
*/
public function getSecure()
{
return $this->data['Secure'];
}
/**
* Set whether or not the cookie is secure
*
* @param bool $secure Set to true or false if secure
*
* @return self
*/
public function setSecure($secure)
{
$this->data['Secure'] = $secure;
return $this;
}
/**
* Get whether or not this is a session cookie
*
* @return null|bool
*/
public function getDiscard()
{
return $this->data['Discard'];
}
/**
* Set whether or not this is a session cookie
*
* @param bool $discard Set to true or false if this is a session cookie
*
* @return self
*/
public function setDiscard($discard)
{
$this->data['Discard'] = $discard;
return $this;
}
/**
* Get whether or not this is an HTTP only cookie
*
* @return bool
*/
public function getHttpOnly()
{
return $this->data['HttpOnly'];
}
/**
* Set whether or not this is an HTTP only cookie
*
* @param bool $httpOnly Set to true or false if this is HTTP only
*
* @return self
*/
public function setHttpOnly($httpOnly)
{
$this->data['HttpOnly'] = $httpOnly;
return $this;
}
/**
* Check if the cookie matches a path value
*
* @param string $path Path to check against
*
* @return bool
*/
public function matchesPath($path)
{
return !$this->getPath() || 0 === stripos($path, $this->getPath());
}
/**
* Check if the cookie matches a domain value
*
* @param string $domain Domain to check against
*
* @return bool
*/
public function matchesDomain($domain)
{
// Remove the leading '.' as per spec in RFC 6265.
// http://tools.ietf.org/html/rfc6265#section-5.2.3
$cookieDomain = ltrim($this->getDomain(), '.');
// Domain not set or exact match.
if (!$cookieDomain || !strcasecmp($domain, $cookieDomain)) {
return true;
}
// Matching the subdomain according to RFC 6265.
// http://tools.ietf.org/html/rfc6265#section-5.1.3
if (filter_var($domain, FILTER_VALIDATE_IP)) {
return false;
}
return (bool) preg_match('/\.' . preg_quote($cookieDomain) . '$/i', $domain);
}
/**
* Check if the cookie is expired
*
* @return bool
*/
public function isExpired()
{
return $this->getExpires() && time() > $this->getExpires();
}
/**
* Check if the cookie is valid according to RFC 6265
*
* @return bool|string Returns true if valid or an error message if invalid
*/
public function validate()
{
// Names must not be empty, but can be 0
$name = $this->getName();
if (empty($name) && !is_numeric($name)) {
return 'The cookie name must not be empty';
}
// Check if any of the invalid characters are present in the cookie name
if (preg_match("/[=,; \t\r\n\013\014]/", $name)) {
return "Cookie name must not cannot invalid characters: =,; \\t\\r\\n\\013\\014";
}
// Value must not be empty, but can be 0
$value = $this->getValue();
if (empty($value) && !is_numeric($value)) {
return 'The cookie value must not be empty';
}
// Domains must not be empty, but can be 0
// A "0" is not a valid internet domain, but may be used as server name
// in a private network.
$domain = $this->getDomain();
if (empty($domain) && !is_numeric($domain)) {
return 'The cookie domain must not be empty';
}
return true;
}
}

View file

@ -0,0 +1,21 @@
<?php
namespace GuzzleHttp\Event;
/**
* Basic event class that can be extended.
*/
abstract class AbstractEvent implements EventInterface
{
private $propagationStopped = false;
public function isPropagationStopped()
{
return $this->propagationStopped;
}
public function stopPropagation()
{
$this->propagationStopped = true;
}
}

View file

@ -0,0 +1,49 @@
<?php
namespace GuzzleHttp\Event;
use GuzzleHttp\Adapter\TransactionInterface;
use GuzzleHttp\ClientInterface;
use GuzzleHttp\Message\RequestInterface;
abstract class AbstractRequestEvent extends AbstractEvent
{
/** @var TransactionInterface */
private $transaction;
/**
* @param TransactionInterface $transaction
*/
public function __construct(TransactionInterface $transaction)
{
$this->transaction = $transaction;
}
/**
* Get the client associated with the event
*
* @return ClientInterface
*/
public function getClient()
{
return $this->transaction->getClient();
}
/**
* Get the request object
*
* @return RequestInterface
*/
public function getRequest()
{
return $this->transaction->getRequest();
}
/**
* @return TransactionInterface
*/
protected function getTransaction()
{
return $this->transaction;
}
}

View file

@ -0,0 +1,83 @@
<?php
namespace GuzzleHttp\Event;
use GuzzleHttp\Adapter\TransactionInterface;
use GuzzleHttp\Message\ResponseInterface;
/**
* Event that contains transaction statistics (time over the wire, lookup time,
* etc.).
*
* Adapters that create this event SHOULD add, at a minimum, the 'total_time'
* transfer statistic that measures the amount of time, in seconds, taken to
* complete a transfer for the current request/response cycle. Each event
* pertains to a single request/response transaction, NOT the entire
* transaction (e.g. redirects).
*
* Adapters that add transaction statistics SHOULD follow the same string
* attribute names that are provided by cURL listed at
* http://php.net/manual/en/function.curl-getinfo.php. However, not all
* adapters will have access to the advanced statistics provided by cURL. The
* most useful transfer statistics are as follows:
*
* - total_time: Total transaction time in seconds for last transfer
* - namelookup_time: Time in seconds until name resolving was complete
* - connect_time: Time in seconds it took to establish the connection
* - pretransfer_time: Time in seconds from start until just before file
* transfer begins.
* - starttransfer_time: Time in seconds until the first byte is about to be
* transferred.
* - speed_download: Average download speed
* - speed_upload: Average upload speed
*/
abstract class AbstractTransferEvent extends AbstractRequestEvent
{
private $transferInfo;
/**
* @param TransactionInterface $transaction Transaction
* @param array $transferInfo Transfer statistics
*/
public function __construct(
TransactionInterface $transaction,
$transferInfo = []
) {
parent::__construct($transaction);
$this->transferInfo = $transferInfo;
}
/**
* Get all transfer information as an associative array if no $name
* argument is supplied, or gets a specific transfer statistic if
* a $name attribute is supplied (e.g., 'total_time').
*
* @param string $name Name of the transfer stat to retrieve
*
* @return mixed|null|array
*/
public function getTransferInfo($name = null)
{
if (!$name) {
return $this->transferInfo;
}
return isset($this->transferInfo[$name])
? $this->transferInfo[$name]
: null;
}
/**
* Get the response
*
* @return ResponseInterface|null
*/
abstract public function getResponse();
/**
* Intercept the request and associate a response
*
* @param ResponseInterface $response Response to set
*/
abstract public function intercept(ResponseInterface $response);
}

View file

@ -0,0 +1,26 @@
<?php
namespace GuzzleHttp\Event;
use GuzzleHttp\Message\ResponseInterface;
/**
* Event object emitted before a request is sent.
*
* You may change the Response associated with the request using the
* intercept() method of the event.
*/
class BeforeEvent extends AbstractRequestEvent
{
/**
* Intercept the request and associate a response
*
* @param ResponseInterface $response Response to set
*/
public function intercept(ResponseInterface $response)
{
$this->getTransaction()->setResponse($response);
$this->stopPropagation();
RequestEvents::emitComplete($this->getTransaction());
}
}

View file

@ -0,0 +1,35 @@
<?php
namespace GuzzleHttp\Event;
use GuzzleHttp\Message\ResponseInterface;
/**
* Event object emitted after a request has been completed.
*
* You may change the Response associated with the request using the
* intercept() method of the event.
*/
class CompleteEvent extends AbstractTransferEvent
{
/**
* Intercept the request and associate a response
*
* @param ResponseInterface $response Response to set
*/
public function intercept(ResponseInterface $response)
{
$this->stopPropagation();
$this->getTransaction()->setResponse($response);
}
/**
* Get the response of the request
*
* @return ResponseInterface
*/
public function getResponse()
{
return $this->getTransaction()->getResponse();
}
}

View file

@ -0,0 +1,147 @@
<?php
namespace GuzzleHttp\Event;
/**
* Guzzle event emitter.
*
* Some of this class is based on the Symfony EventDispatcher component, which
* ships with the following license:
*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*
* @link https://github.com/symfony/symfony/tree/master/src/Symfony/Component/EventDispatcher
*/
class Emitter implements EmitterInterface
{
/** @var array */
private $listeners = [];
/** @var array */
private $sorted = [];
public function on($eventName, callable $listener, $priority = 0)
{
if ($priority === 'first') {
$priority = isset($this->listeners[$eventName])
? max(array_keys($this->listeners[$eventName])) + 1
: 1;
} elseif ($priority === 'last') {
$priority = isset($this->listeners[$eventName])
? min(array_keys($this->listeners[$eventName])) - 1
: -1;
}
$this->listeners[$eventName][$priority][] = $listener;
unset($this->sorted[$eventName]);
}
public function once($eventName, callable $listener, $priority = 0)
{
$onceListener = function (
EventInterface $event,
$eventName
) use (&$onceListener, $eventName, $listener, $priority) {
$this->removeListener($eventName, $onceListener);
$listener($event, $eventName, $this);
};
$this->on($eventName, $onceListener, $priority);
}
public function removeListener($eventName, callable $listener)
{
if (!isset($this->listeners[$eventName])) {
return;
}
foreach ($this->listeners[$eventName] as $priority => $listeners) {
if (false !== ($key = array_search($listener, $listeners, true))) {
unset(
$this->listeners[$eventName][$priority][$key],
$this->sorted[$eventName]
);
}
}
}
public function listeners($eventName = null)
{
// Return all events in a sorted priority order
if ($eventName === null) {
foreach (array_keys($this->listeners) as $eventName) {
if (!isset($this->sorted[$eventName])) {
$this->listeners($eventName);
}
}
return $this->sorted;
}
// Return the listeners for a specific event, sorted in priority order
if (!isset($this->sorted[$eventName])) {
if (!isset($this->listeners[$eventName])) {
return [];
} else {
krsort($this->listeners[$eventName]);
$this->sorted[$eventName] = call_user_func_array(
'array_merge',
$this->listeners[$eventName]
);
}
}
return $this->sorted[$eventName];
}
public function emit($eventName, EventInterface $event)
{
if (isset($this->listeners[$eventName])) {
foreach ($this->listeners($eventName) as $listener) {
$listener($event, $eventName);
if ($event->isPropagationStopped()) {
break;
}
}
}
return $event;
}
public function attach(SubscriberInterface $subscriber)
{
foreach ($subscriber->getEvents() as $eventName => $listener) {
$this->on(
$eventName,
array($subscriber, $listener[0]),
isset($listener[1]) ? $listener[1] : 0
);
}
}
public function detach(SubscriberInterface $subscriber)
{
foreach ($subscriber->getEvents() as $eventName => $listener) {
$this->removeListener($eventName, array($subscriber, $listener[0]));
}
}
public function __call($name, $arguments)
{
return \GuzzleHttp\deprecation_proxy(
$this,
$name,
$arguments,
[
'addSubscriber' => 'attach',
'removeSubscriber' => 'detach',
'addListener' => 'on',
'dispatch' => 'emit'
]
);
}
}

View file

@ -0,0 +1,88 @@
<?php
namespace GuzzleHttp\Event;
/**
* Guzzle event emitter.
*/
interface EmitterInterface
{
/**
* Binds a listener to a specific event.
*
* @param string $eventName Name of the event to bind to.
* @param callable $listener Listener to invoke when triggered.
* @param int|string $priority The higher this value, the earlier an event
* listener will be triggered in the chain (defaults to 0). You can
* pass "first" or "last" to dynamically specify the event priority
* based on the current event priorities associated with the given
* event name in the emitter. Use "first" to set the priority to the
* current highest priority plus one. Use "last" to set the priority to
* the current lowest event priority minus one.
*/
public function on($eventName, callable $listener, $priority = 0);
/**
* Binds a listener to a specific event. After the listener is triggered
* once, it is removed as a listener.
*
* @param string $eventName Name of the event to bind to.
* @param callable $listener Listener to invoke when triggered.
* @param int $priority The higher this value, the earlier an event
* listener will be triggered in the chain (defaults to 0)
*/
public function once($eventName, callable $listener, $priority = 0);
/**
* Removes an event listener from the specified event.
*
* @param string $eventName The event to remove a listener from
* @param callable $listener The listener to remove
*/
public function removeListener($eventName, callable $listener);
/**
* Gets the listeners of a specific event or all listeners if no event is
* specified.
*
* @param string $eventName The name of the event. Pass null (the default)
* to retrieve all listeners.
*
* @return array The event listeners for the specified event, or all event
* listeners by event name. The format of the array when retrieving a
* specific event list is an array of callables. The format of the array
* when retrieving all listeners is an associative array of arrays of
* callables.
*/
public function listeners($eventName = null);
/**
* Emits an event to all registered listeners.
*
* Each event that is bound to the emitted eventName receives a
* EventInterface, the name of the event, and the event emitter.
*
* @param string $eventName The name of the event to dispatch.
* @param EventInterface $event The event to pass to the event handlers/listeners.
*
* @return EventInterface Returns the provided event object
*/
public function emit($eventName, EventInterface $event);
/**
* Attaches an event subscriber.
*
* The subscriber is asked for all the events it is interested in and added
* as an event listener for each event.
*
* @param SubscriberInterface $subscriber Subscriber to attach.
*/
public function attach(SubscriberInterface $subscriber);
/**
* Detaches an event subscriber.
*
* @param SubscriberInterface $subscriber Subscriber to detach.
*/
public function detach(SubscriberInterface $subscriber);
}

View file

@ -0,0 +1,65 @@
<?php
namespace GuzzleHttp\Event;
use GuzzleHttp\Exception\RequestException;
use GuzzleHttp\Message\ResponseInterface;
use GuzzleHttp\Adapter\TransactionInterface;
/**
* Event object emitted after a request has been sent and an error was
* encountered.
*
* You may intercept the exception and inject a response into the event to
* rescue the request.
*/
class ErrorEvent extends AbstractTransferEvent
{
private $exception;
/**
* @param TransactionInterface $transaction Transaction that contains the request
* @param RequestException $e Exception encountered
* @param array $transferStats Array of transfer statistics
*/
public function __construct(
TransactionInterface $transaction,
RequestException $e,
$transferStats = []
) {
parent::__construct($transaction, $transferStats);
$this->exception = $e;
}
/**
* Intercept the exception and inject a response
*
* @param ResponseInterface $response Response to set
*/
public function intercept(ResponseInterface $response)
{
$this->stopPropagation();
$this->getTransaction()->setResponse($response);
RequestEvents::emitComplete($this->getTransaction());
}
/**
* Get the exception that was encountered
*
* @return RequestException
*/
public function getException()
{
return $this->exception;
}
/**
* Get the response the was received (if any)
*
* @return ResponseInterface|null
*/
public function getResponse()
{
return $this->getException()->getResponse();
}
}

View file

@ -0,0 +1,24 @@
<?php
namespace GuzzleHttp\Event;
/**
* Base event interface used when dispatching events to listeners using an
* event emitter.
*/
interface EventInterface
{
/**
* Returns whether or not stopPropagation was called on the event.
*
* @return bool
* @see Event::stopPropagation
*/
public function isPropagationStopped();
/**
* Stops the propagation of the event, preventing subsequent listeners
* registered to the same event from being invoked.
*/
public function stopPropagation();
}

View file

@ -0,0 +1,16 @@
<?php
namespace GuzzleHttp\Event;
/**
* Holds an event emitter
*/
interface HasEmitterInterface
{
/**
* Get the event emitter of the object
*
* @return EmitterInterface
*/
public function getEmitter();
}

View file

@ -0,0 +1,21 @@
<?php
namespace GuzzleHttp\Event;
/**
* Trait that implements the methods of HasEmitterInterface
*/
trait HasEmitterTrait
{
/** @var EmitterInterface */
private $emitter;
public function getEmitter()
{
if (!$this->emitter) {
$this->emitter = new Emitter();
}
return $this->emitter;
}
}

View file

@ -0,0 +1,39 @@
<?php
namespace GuzzleHttp\Event;
use GuzzleHttp\Message\ResponseInterface;
use GuzzleHttp\Adapter\TransactionInterface;
/**
* Event object emitted after the response headers of a request have been
* received.
*
* You may intercept the exception and inject a response into the event to
* rescue the request.
*/
class HeadersEvent extends AbstractRequestEvent
{
/**
* @param TransactionInterface $transaction Transaction that contains the
* request and response.
* @throws \RuntimeException
*/
public function __construct(TransactionInterface $transaction)
{
parent::__construct($transaction);
if (!$transaction->getResponse()) {
throw new \RuntimeException('A response must be present');
}
}
/**
* Get the response the was received
*
* @return ResponseInterface
*/
public function getResponse()
{
return $this->getTransaction()->getResponse();
}
}

View file

@ -0,0 +1,89 @@
<?php
namespace GuzzleHttp\Event;
/**
* Trait that provides methods for extract event listeners specified in an array
* and attaching them to an emitter owned by the object or one of its direct
* dependencies.
*/
trait ListenerAttacherTrait
{
/**
* Attaches event listeners and properly sets their priorities and whether
* or not they are are only executed once.
*
* @param HasEmitterInterface $object Object that has the event emitter.
* @param array $listeners Array of hashes representing event
* event listeners. Each item contains
* "name", "fn", "priority", & "once".
*/
private function attachListeners(HasEmitterInterface $object, array $listeners)
{
$emitter = $object->getEmitter();
foreach ($listeners as $el) {
if ($el['once']) {
$emitter->once($el['name'], $el['fn'], $el['priority']);
} else {
$emitter->on($el['name'], $el['fn'], $el['priority']);
}
}
}
/**
* Extracts the allowed events from the provided array, and ignores anything
* else in the array. The event listener must be specified as a callable or
* as an array of event listener data ("name", "fn", "priority", "once").
*
* @param array $source Array containing callables or hashes of data to be
* prepared as event listeners.
* @param array $events Names of events to look for in the provided $source
* array. Other keys are ignored.
* @return array
*/
private function prepareListeners(array $source, array $events)
{
$listeners = [];
foreach ($events as $name) {
if (isset($source[$name])) {
$this->buildListener($name, $source[$name], $listeners);
}
}
return $listeners;
}
/**
* Creates a complete event listener definition from the provided array of
* listener data. Also works recursively if more than one listeners are
* contained in the provided array.
*
* @param string $name Name of the event the listener is for.
* @param array|callable $data Event listener data to prepare.
* @param array $listeners Array of listeners, passed by reference.
*
* @throws \InvalidArgumentException if the event data is malformed.
*/
private function buildListener($name, $data, &$listeners)
{
static $defaults = ['priority' => 0, 'once' => false];
// If a callable is provided, normalize it to the array format.
if (is_callable($data)) {
$data = ['fn' => $data];
}
// Prepare the listener and add it to the array, recursively.
if (isset($data['fn'])) {
$data['name'] = $name;
$listeners[] = $data + $defaults;
} elseif (is_array($data)) {
foreach ($data as $listenerData) {
$this->buildListener($name, $listenerData, $listeners);
}
} else {
throw new \InvalidArgumentException('Each event listener must be a '
. 'callable or an associative array containing a "fn" key.');
}
}
}

View file

@ -0,0 +1,162 @@
<?php
namespace GuzzleHttp\Event;
use GuzzleHttp\Adapter\TransactionInterface;
use GuzzleHttp\Exception\RequestException;
/**
* Contains methods used to manage the request event lifecycle.
*/
final class RequestEvents
{
// Generic event priorities
const EARLY = 10000;
const LATE = -10000;
// "before" priorities
const PREPARE_REQUEST = -100;
const SIGN_REQUEST = -10000;
// "complete" and "error" response priorities
const VERIFY_RESPONSE = 100;
const REDIRECT_RESPONSE = 200;
/**
* Emits the before send event for a request and emits an error
* event if an error is encountered during the before send.
*
* @param TransactionInterface $transaction
*
* @throws RequestException
*/
public static function emitBefore(TransactionInterface $transaction) {
$request = $transaction->getRequest();
try {
$request->getEmitter()->emit(
'before',
new BeforeEvent($transaction)
);
} catch (RequestException $e) {
// When a RequestException has been emitted through emitError, the
// exception is marked as "emitted". This means that the exception
// had a chance to be rescued but was not. In this case, this method
// must not emit the error again, but rather throw the exception.
// This prevents RequestExceptions encountered during the before
// event from being emitted to listeners twice.
if ($e->emittedError()) {
throw $e;
}
self::emitError($transaction, $e);
} catch (\Exception $e) {
self::emitError($transaction, $e);
}
}
/**
* Emits the complete event for a request and emits an error
* event if an error is encountered during the after send.
*
* @param TransactionInterface $transaction Transaction to emit for
* @param array $stats Transfer stats
*
* @throws RequestException
*/
public static function emitComplete(
TransactionInterface $transaction,
array $stats = []
) {
$request = $transaction->getRequest();
$transaction->getResponse()->setEffectiveUrl($request->getUrl());
try {
$request->getEmitter()->emit(
'complete',
new CompleteEvent($transaction, $stats)
);
} catch (RequestException $e) {
self::emitError($transaction, $e, $stats);
}
}
/**
* Emits the headers event for a request.
*
* @param TransactionInterface $transaction Transaction to emit for
*/
public static function emitHeaders(TransactionInterface $transaction)
{
$transaction->getRequest()->getEmitter()->emit(
'headers',
new HeadersEvent($transaction)
);
}
/**
* Emits an error event for a request and accounts for the propagation
* of an error event being stopped to prevent the exception from being
* thrown.
*
* @param TransactionInterface $transaction
* @param \Exception $e
* @param array $stats
*
* @throws \GuzzleHttp\Exception\RequestException
*/
public static function emitError(
TransactionInterface $transaction,
\Exception $e,
array $stats = []
) {
$request = $transaction->getRequest();
// Convert non-request exception to a wrapped exception
if (!($e instanceof RequestException)) {
$e = new RequestException($e->getMessage(), $request, null, $e);
}
// Mark the exception as having been emitted for an error event. This
// works in tandem with the emitBefore method to prevent the error
// event from being triggered twice for the same exception.
$e->emittedError(true);
// Dispatch an event and allow interception
if (!$request->getEmitter()->emit(
'error',
new ErrorEvent($transaction, $e, $stats)
)->isPropagationStopped()) {
throw $e;
}
}
/**
* Converts an array of event options into a formatted array of valid event
* configuration.
*
* @param array $options Event array to convert
* @param array $events Event names to convert in the options array.
* @param mixed $handler Event handler to utilize
*
* @return array
* @throws \InvalidArgumentException if the event config is invalid
* @internal
*/
public static function convertEventArray(
array $options,
array $events,
$handler
) {
foreach ($events as $name) {
if (!isset($options[$name])) {
$options[$name] = $handler;
} elseif (is_callable($options[$name])) {
$options[$name] = [['fn' => $options[$name]], $handler];
} elseif (is_array($options[$name])) {
$options[$name][] = $handler;
} else {
throw new \InvalidArgumentException('Invalid event format');
}
}
return $options;
}
}

View file

@ -0,0 +1,31 @@
<?php
namespace GuzzleHttp\Event;
/**
* SubscriberInterface provides an array of events to an
* EventEmitterInterface when it is registered. The emitter then binds the
* listeners specified by the EventSubscriber.
*
* This interface is based on the SubscriberInterface of the Symfony.
* @link https://github.com/symfony/symfony/tree/master/src/Symfony/Component/EventDispatcher
*/
interface SubscriberInterface
{
/**
* Returns an array of event names this subscriber wants to listen to.
*
* The returned array keys MUST map to an event name. Each array value
* MUST be an array in which the first element is the name of a function
* on the EventSubscriber. The second element in the array is optional, and
* if specified, designates the event priority.
*
* For example:
*
* - ['eventName' => ['methodName']]
* - ['eventName' => ['methodName', $priority]]
*
* @return array
*/
public function getEvents();
}

View file

@ -0,0 +1,5 @@
<?php
namespace GuzzleHttp\Exception;
class AdapterException extends TransferException {}

View file

@ -0,0 +1,8 @@
<?php
namespace GuzzleHttp\Exception;
/**
* Exception when an HTTP error occurs (4xx or 5xx error)
*/
class BadResponseException extends RequestException {}

View file

@ -0,0 +1,8 @@
<?php
namespace GuzzleHttp\Exception;
/**
* Exception when a client error is encountered (4xx codes)
*/
class ClientException extends BadResponseException {}

View file

@ -0,0 +1,5 @@
<?php
namespace GuzzleHttp\Exception;
class CouldNotRewindStreamException extends RequestException {}

View file

@ -0,0 +1,32 @@
<?php
namespace GuzzleHttp\Exception;
use GuzzleHttp\Message\ResponseInterface;
/**
* Exception when a client is unable to parse the response body as XML or JSON
*/
class ParseException extends TransferException
{
/** @var ResponseInterface */
private $response;
public function __construct(
$message = '',
ResponseInterface $response = null,
\Exception $previous = null
) {
parent::__construct($message, 0, $previous);
$this->response = $response;
}
/**
* Get the associated response
*
* @return ResponseInterface|null
*/
public function getResponse()
{
return $this->response;
}
}

View file

@ -0,0 +1,124 @@
<?php
namespace GuzzleHttp\Exception;
use GuzzleHttp\Message\RequestInterface;
use GuzzleHttp\Message\ResponseInterface;
/**
* HTTP Request exception
*/
class RequestException extends TransferException
{
/** @var bool */
private $emittedErrorEvent = false;
/** @var RequestInterface */
private $request;
/** @var ResponseInterface */
private $response;
public function __construct(
$message = '',
RequestInterface $request,
ResponseInterface $response = null,
\Exception $previous = null
) {
$code = $response ? $response->getStatusCode() : 0;
parent::__construct($message, $code, $previous);
$this->request = $request;
$this->response = $response;
}
/**
* Factory method to create a new exception with a normalized error message
*
* @param RequestInterface $request Request
* @param ResponseInterface $response Response received
* @param \Exception $previous Previous exception
*
* @return self
*/
public static function create(
RequestInterface $request,
ResponseInterface $response = null,
\Exception $previous = null
) {
if (!$response) {
return new self('Error completing request', $request, null, $previous);
}
$level = $response->getStatusCode()[0];
if ($level == '4') {
$label = 'Client error response';
$className = __NAMESPACE__ . '\\ClientException';
} elseif ($level == '5') {
$label = 'Server error response';
$className = __NAMESPACE__ . '\\ServerException';
} else {
$label = 'Unsuccessful response';
$className = __CLASS__;
}
$message = $label . ' [url] ' . $request->getUrl()
. ' [status code] ' . $response->getStatusCode()
. ' [reason phrase] ' . $response->getReasonPhrase();
return new $className($message, $request, $response, $previous);
}
/**
* Get the request that caused the exception
*
* @return RequestInterface
*/
public function getRequest()
{
return $this->request;
}
/**
* Get the associated response
*
* @return ResponseInterface|null
*/
public function getResponse()
{
return $this->response;
}
/**
* Check if a response was received
*
* @return bool
*/
public function hasResponse()
{
return $this->response !== null;
}
/**
* Check or set if the exception was emitted in an error event.
*
* This value is used in the RequestEvents::emitBefore() method to check
* to see if an exception has already been emitted in an error event.
*
* @param bool|null Set to true to set the exception as having emitted an
* error. Leave null to retrieve the current setting.
*
* @return null|bool
* @throws \InvalidArgumentException if you attempt to set the value to false
*/
public function emittedError($value = null)
{
if ($value === null) {
return $this->emittedErrorEvent;
} elseif ($value === true) {
return $this->emittedErrorEvent = true;
} else {
throw new \InvalidArgumentException('You cannot set the emitted '
. 'error value to false.');
}
}
}

View file

@ -0,0 +1,8 @@
<?php
namespace GuzzleHttp\Exception;
/**
* Exception when a server error is encountered (5xx codes)
*/
class ServerException extends BadResponseException {}

View file

@ -0,0 +1,5 @@
<?php
namespace GuzzleHttp\Exception;
class TooManyRedirectsException extends RequestException {}

View file

@ -0,0 +1,5 @@
<?php
namespace GuzzleHttp\Exception;
class TransferException extends \RuntimeException {}

View file

@ -0,0 +1,76 @@
<?php
namespace GuzzleHttp;
/**
* Trait implementing ToArrayInterface, \ArrayAccess, \Countable,
* \IteratorAggregate, and some path style methods.
*/
trait HasDataTrait
{
/** @var array */
protected $data = [];
public function getIterator()
{
return new \ArrayIterator($this->data);
}
public function offsetGet($offset)
{
return isset($this->data[$offset]) ? $this->data[$offset] : null;
}
public function offsetSet($offset, $value)
{
$this->data[$offset] = $value;
}
public function offsetExists($offset)
{
return isset($this->data[$offset]);
}
public function offsetUnset($offset)
{
unset($this->data[$offset]);
}
public function toArray()
{
return $this->data;
}
public function count()
{
return count($this->data);
}
/**
* Get a value from the collection using a path syntax to retrieve nested
* data.
*
* @param string $path Path to traverse and retrieve a value from
*
* @return mixed|null
*/
public function getPath($path)
{
return \GuzzleHttp\get_path($this->data, $path);
}
/**
* Set a value into a nested array key. Keys will be created as needed to
* set the value.
*
* @param string $path Path to set
* @param mixed $value Value to set at the key
*
* @throws \RuntimeException when trying to setPath using a nested path
* that travels through a scalar value
*/
public function setPath($path, $value)
{
\GuzzleHttp\set_path($this->data, $path, $value);
}
}

View file

@ -0,0 +1,237 @@
<?php
namespace GuzzleHttp\Message;
use GuzzleHttp\Stream\StreamInterface;
abstract class AbstractMessage implements MessageInterface
{
/** @var array HTTP header collection */
private $headers = [];
/** @var array mapping a lowercase header name to its name over the wire */
private $headerNames = [];
/** @var StreamInterface Message body */
private $body;
/** @var string HTTP protocol version of the message */
private $protocolVersion = '1.1';
public function __toString()
{
$result = $this->getStartLine();
foreach ($this->getHeaders() as $name => $values) {
$result .= "\r\n{$name}: " . implode(', ', $values);
}
return $result . "\r\n\r\n" . $this->body;
}
public function getProtocolVersion()
{
return $this->protocolVersion;
}
public function getBody()
{
return $this->body;
}
public function setBody(StreamInterface $body = null)
{
if ($body === null) {
// Setting a null body will remove the body of the request
$this->removeHeader('Content-Length')
->removeHeader('Transfer-Encoding');
}
$this->body = $body;
return $this;
}
public function addHeader($header, $value)
{
static $valid = ['string' => true, 'integer' => true,
'double' => true, 'array' => true];
$type = gettype($value);
if (!isset($valid[$type])) {
throw new \InvalidArgumentException('Invalid header value');
}
if ($type == 'array') {
$current = array_merge($this->getHeader($header, true), $value);
} else {
$current = $this->getHeader($header, true);
$current[] = $value;
}
return $this->setHeader($header, $current);
}
public function addHeaders(array $headers)
{
foreach ($headers as $name => $header) {
$this->addHeader($name, $header);
}
}
public function getHeader($header, $asArray = false)
{
$name = strtolower($header);
if (!isset($this->headers[$name])) {
return $asArray ? [] : '';
}
return $asArray
? $this->headers[$name]
: implode(', ', $this->headers[$name]);
}
public function getHeaders()
{
$headers = [];
foreach ($this->headers as $name => $values) {
$headers[$this->headerNames[$name]] = $values;
}
return $headers;
}
public function setHeader($header, $value)
{
$header = trim($header);
$name = strtolower($header);
$this->headerNames[$name] = $header;
switch (gettype($value)) {
case 'string':
$this->headers[$name] = [trim($value)];
break;
case 'integer':
case 'double':
$this->headers[$name] = [(string) $value];
break;
case 'array':
foreach ($value as &$v) {
$v = trim($v);
}
$this->headers[$name] = $value;
break;
default:
throw new \InvalidArgumentException('Invalid header value '
. 'provided: ' . var_export($value, true));
}
return $this;
}
public function setHeaders(array $headers)
{
$this->headers = $this->headerNames = [];
foreach ($headers as $key => $value) {
$this->setHeader($key, $value);
}
return $this;
}
public function hasHeader($header)
{
return isset($this->headers[strtolower($header)]);
}
public function removeHeader($header)
{
$name = strtolower($header);
unset($this->headers[$name], $this->headerNames[$name]);
return $this;
}
/**
* Parse an array of header values containing ";" separated data into an
* array of associative arrays representing the header key value pair
* data of the header. When a parameter does not contain a value, but just
* contains a key, this function will inject a key with a '' string value.
*
* @param MessageInterface $message That contains the header
* @param string $header Header to retrieve from the message
*
* @return array Returns the parsed header values.
*/
public static function parseHeader(MessageInterface $message, $header)
{
static $trimmed = "\"' \n\t\r";
$params = $matches = [];
foreach (self::normalizeHeader($message, $header) as $val) {
$part = [];
foreach (preg_split('/;(?=([^"]*"[^"]*")*[^"]*$)/', $val) as $kvp) {
if (preg_match_all('/<[^>]+>|[^=]+/', $kvp, $matches)) {
$m = $matches[0];
if (isset($m[1])) {
$part[trim($m[0], $trimmed)] = trim($m[1], $trimmed);
} else {
$part[] = trim($m[0], $trimmed);
}
}
}
if ($part) {
$params[] = $part;
}
}
return $params;
}
/**
* Converts an array of header values that may contain comma separated
* headers into an array of headers with no comma separated values.
*
* @param MessageInterface $message That contains the header
* @param string $header Header to retrieve from the message
*
* @return array Returns the normalized header field values.
*/
public static function normalizeHeader(MessageInterface $message, $header)
{
$h = $message->getHeader($header, true);
for ($i = 0, $total = count($h); $i < $total; $i++) {
if (strpos($h[$i], ',') === false) {
continue;
}
foreach (preg_split('/,(?=([^"]*"[^"]*")*[^"]*$)/', $h[$i]) as $v) {
$h[] = trim($v);
}
unset($h[$i]);
}
return $h;
}
/**
* Returns the start line of a message.
*
* @return string
*/
abstract protected function getStartLine();
/**
* Accepts and modifies the options provided to the message in the
* constructor.
*
* Can be overridden in subclasses as necessary.
*
* @param array $options Options array passed by reference.
*/
protected function handleOptions(array &$options)
{
if (isset($options['protocol_version'])) {
$this->protocolVersion = $options['protocol_version'];
}
}
}

View file

@ -0,0 +1,345 @@
<?php
namespace GuzzleHttp\Message;
use GuzzleHttp\Event\ListenerAttacherTrait;
use GuzzleHttp\Post\PostFileInterface;
use GuzzleHttp\Subscriber\Cookie;
use GuzzleHttp\Cookie\CookieJar;
use GuzzleHttp\Cookie\CookieJarInterface;
use GuzzleHttp\Subscriber\HttpError;
use GuzzleHttp\Post\PostBody;
use GuzzleHttp\Post\PostFile;
use GuzzleHttp\Subscriber\Redirect;
use GuzzleHttp\Stream;
use GuzzleHttp\Query;
use GuzzleHttp\Url;
/**
* Default HTTP request factory used to create Request and Response objects.
*/
class MessageFactory implements MessageFactoryInterface
{
use ListenerAttacherTrait;
/** @var HttpError */
private $errorPlugin;
/** @var Redirect */
private $redirectPlugin;
/** @var array */
protected static $classMethods = [];
public function __construct()
{
$this->errorPlugin = new HttpError();
$this->redirectPlugin = new Redirect();
}
public function createResponse(
$statusCode,
array $headers = [],
$body = null,
array $options = []
) {
if (null !== $body) {
$body = Stream\create($body);
}
return new Response($statusCode, $headers, $body, $options);
}
public function createRequest($method, $url, array $options = [])
{
// Handle the request protocol version option that needs to be
// specified in the request constructor.
if (isset($options['version'])) {
$options['config']['protocol_version'] = $options['version'];
unset($options['version']);
}
$request = new Request($method, $url, [], null,
isset($options['config']) ? $options['config'] : []);
unset($options['config']);
// Use a POST body by default
if ($method == 'POST' &&
!isset($options['body']) &&
!isset($options['json'])
) {
$options['body'] = [];
}
if ($options) {
$this->applyOptions($request, $options);
}
return $request;
}
/**
* Create a request or response object from an HTTP message string
*
* @param string $message Message to parse
*
* @return RequestInterface|ResponseInterface
* @throws \InvalidArgumentException if unable to parse a message
*/
public function fromMessage($message)
{
static $parser;
if (!$parser) {
$parser = new MessageParser();
}
// Parse a response
if (strtoupper(substr($message, 0, 4)) == 'HTTP') {
$data = $parser->parseResponse($message);
return $this->createResponse(
$data['code'],
$data['headers'],
$data['body'] === '' ? null : $data['body'],
$data
);
}
// Parse a request
if (!($data = ($parser->parseRequest($message)))) {
throw new \InvalidArgumentException('Unable to parse request');
}
return $this->createRequest(
$data['method'],
Url::buildUrl($data['request_url']),
[
'headers' => $data['headers'],
'body' => $data['body'] === '' ? null : $data['body'],
'config' => [
'protocol_version' => $data['protocol_version']
]
]
);
}
/**
* Apply POST fields and files to a request to attempt to give an accurate
* representation.
*
* @param RequestInterface $request Request to update
* @param array $body Body to apply
*/
protected function addPostData(RequestInterface $request, array $body)
{
static $fields = ['string' => true, 'array' => true, 'NULL' => true,
'boolean' => true, 'double' => true, 'integer' => true];
$post = new PostBody();
foreach ($body as $key => $value) {
if (isset($fields[gettype($value)])) {
$post->setField($key, $value);
} elseif ($value instanceof PostFileInterface) {
$post->addFile($value);
} else {
$post->addFile(new PostFile($key, $value));
}
}
$request->setBody($post);
$post->applyRequestHeaders($request);
}
protected function applyOptions(
RequestInterface $request,
array $options = []
) {
// Values specified in the config map are passed to request options
static $configMap = ['connect_timeout' => 1, 'timeout' => 1,
'verify' => 1, 'ssl_key' => 1, 'cert' => 1, 'proxy' => 1,
'debug' => 1, 'save_to' => 1, 'stream' => 1, 'expect' => 1];
// Take the class of the instance, not the parent
$selfClass = get_class($this);
// Check if we already took it's class methods and had them saved
if (!isset(self::$classMethods[$selfClass])) {
self::$classMethods[$selfClass] = array_flip(get_class_methods($this));
}
// Take class methods of this particular instance
$methods = self::$classMethods[$selfClass];
// Iterate over each key value pair and attempt to apply a config using
// double dispatch.
$config = $request->getConfig();
foreach ($options as $key => $value) {
$method = "add_{$key}";
if (isset($methods[$method])) {
$this->{$method}($request, $value);
} elseif (isset($configMap[$key])) {
$config[$key] = $value;
} else {
throw new \InvalidArgumentException("No method is configured "
. "to handle the {$key} config key");
}
}
}
private function add_body(RequestInterface $request, $value)
{
if ($value !== null) {
if (is_array($value)) {
$this->addPostData($request, $value);
} else {
$request->setBody(Stream\create($value));
}
}
}
private function add_allow_redirects(RequestInterface $request, $value)
{
static $defaultRedirect = [
'max' => 5,
'strict' => false,
'referer' => false
];
if ($value === false) {
return;
}
if ($value === true) {
$value = $defaultRedirect;
} elseif (!isset($value['max'])) {
throw new \InvalidArgumentException('allow_redirects must be '
. 'true, false, or an array that contains the \'max\' key');
} else {
// Merge the default settings with the provided settings
$value += $defaultRedirect;
}
$request->getConfig()['redirect'] = $value;
$request->getEmitter()->attach($this->redirectPlugin);
}
private function add_exceptions(RequestInterface $request, $value)
{
if ($value === true) {
$request->getEmitter()->attach($this->errorPlugin);
}
}
private function add_auth(RequestInterface $request, $value)
{
if (!$value) {
return;
} elseif (is_array($value)) {
$authType = isset($value[2]) ? strtolower($value[2]) : 'basic';
} else {
$authType = strtolower($value);
}
$request->getConfig()->set('auth', $value);
if ($authType == 'basic') {
$request->setHeader(
'Authorization',
'Basic ' . base64_encode("$value[0]:$value[1]")
);
} elseif ($authType == 'digest') {
// Currently only implemented by the cURL adapter.
// @todo: Need an event listener solution that does not rely on cURL
$config = $request->getConfig();
$config->setPath('curl/' . CURLOPT_HTTPAUTH, CURLAUTH_DIGEST);
$config->setPath('curl/' . CURLOPT_USERPWD, "$value[0]:$value[1]");
}
}
private function add_query(RequestInterface $request, $value)
{
if ($value instanceof Query) {
$original = $request->getQuery();
// Do not overwrite existing query string variables by overwriting
// the object with the query string data passed in the URL
$request->setQuery($value->overwriteWith($original->toArray()));
} elseif (is_array($value)) {
// Do not overwrite existing query string variables
$query = $request->getQuery();
foreach ($value as $k => $v) {
if (!isset($query[$k])) {
$query[$k] = $v;
}
}
} else {
throw new \InvalidArgumentException('query value must be an array '
. 'or Query object');
}
}
private function add_headers(RequestInterface $request, $value)
{
if (!is_array($value)) {
throw new \InvalidArgumentException('header value must be an array');
}
// Do not overwrite existing headers
foreach ($value as $k => $v) {
if (!$request->hasHeader($k)) {
$request->setHeader($k, $v);
}
}
}
private function add_cookies(RequestInterface $request, $value)
{
if ($value === true) {
static $cookie = null;
if (!$cookie) {
$cookie = new Cookie();
}
$request->getEmitter()->attach($cookie);
} elseif (is_array($value)) {
$request->getEmitter()->attach(
new Cookie(CookieJar::fromArray($value, $request->getHost()))
);
} elseif ($value instanceof CookieJarInterface) {
$request->getEmitter()->attach(new Cookie($value));
} elseif ($value !== false) {
throw new \InvalidArgumentException('cookies must be an array, '
. 'true, or a CookieJarInterface object');
}
}
private function add_events(RequestInterface $request, $value)
{
if (!is_array($value)) {
throw new \InvalidArgumentException('events value must be an array');
}
$this->attachListeners($request, $this->prepareListeners($value,
['before', 'complete', 'error', 'headers']
));
}
private function add_subscribers(RequestInterface $request, $value)
{
if (!is_array($value)) {
throw new \InvalidArgumentException('subscribers must be an array');
}
$emitter = $request->getEmitter();
foreach ($value as $subscribers) {
$emitter->attach($subscribers);
}
}
private function add_json(RequestInterface $request, $value)
{
if (!$request->hasHeader('Content-Type')) {
$request->setHeader('Content-Type', 'application/json');
}
$request->setBody(Stream\create(json_encode($value)));
}
}

View file

@ -0,0 +1,71 @@
<?php
namespace GuzzleHttp\Message;
use GuzzleHttp\Url;
/**
* Request and response factory
*/
interface MessageFactoryInterface
{
/**
* Creates a response
*
* @param string $statusCode HTTP status code
* @param array $headers Response headers
* @param mixed $body Response body
* @param array $options Response options
* - protocol_version: HTTP protocol version
* - header_factory: Factory used to create headers
* - And any other options used by a concrete message implementation
*
* @return ResponseInterface
*/
public function createResponse(
$statusCode,
array $headers = [],
$body = null,
array $options = []
);
/**
* Create a new request based on the HTTP method.
*
* This method accepts an associative array of request options. Below is a
* brief description of each parameter. See
* http://docs.guzzlephp.org/clients.html#request-options for a much more
* in-depth description of each parameter.
*
* - headers: Associative array of headers to add to the request
* - body: string|resource|array|StreamInterface request body to send
* - json: mixed Uploads JSON encoded data using an application/json Content-Type header.
* - query: Associative array of query string values to add to the request
* - auth: array|string HTTP auth settings (user, pass[, type="basic"])
* - version: The HTTP protocol version to use with the request
* - cookies: true|false|CookieJarInterface To enable or disable cookies
* - allow_redirects: true|false|array Controls HTTP redirects
* - save_to: string|resource|StreamInterface Where the response is saved
* - events: Associative array of event names to callables or arrays
* - subscribers: Array of event subscribers to add to the request
* - exceptions: Specifies whether or not exceptions are thrown for HTTP protocol errors
* - timeout: Timeout of the request in seconds. Use 0 to wait indefinitely
* - connect_timeout: Number of seconds to wait while trying to connect. (0 to wait indefinitely)
* - verify: SSL validation. True/False or the path to a PEM file
* - cert: Path a SSL cert or array of (path, pwd)
* - ssl_key: Path to a private SSL key or array of (path, pwd)
* - proxy: Specify an HTTP proxy or hash of protocols to proxies
* - debug: Set to true or a resource to view adapter specific debug info
* - stream: Set to true to stream a response body rather than download it all up front
* - expect: true/false/integer Controls the "Expect: 100-Continue" header
* - config: Associative array of request config collection options
*
* @param string $method HTTP method (GET, POST, PUT, etc.)
* @param string|Url $url HTTP URL to connect to
* @param array $options Array of options to apply to the request
*
* @return RequestInterface
* @link http://docs.guzzlephp.org/clients.html#request-options
*/
public function createRequest($method, $url, array $options = []);
}

View file

@ -0,0 +1,148 @@
<?php
namespace GuzzleHttp\Message;
use GuzzleHttp\Stream\StreamInterface;
/**
* Request and response message interface
*/
interface MessageInterface
{
/**
* Get a string representation of the message
*
* @return string
*/
public function __toString();
/**
* Get the HTTP protocol version of the message
*
* @return string
*/
public function getProtocolVersion();
/**
* Sets the body of the message.
*
* The body MUST be a StreamInterface object. Setting the body to null MUST
* remove the existing body.
*
* @param StreamInterface|null $body Body.
*
* @return self Returns the message.
*/
public function setBody(StreamInterface $body = null);
/**
* Get the body of the message
*
* @return StreamInterface|null
*/
public function getBody();
/**
* Gets all message headers.
*
* The keys represent the header name as it will be sent over the wire, and
* each value is an array of strings associated with the header.
*
* // Represent the headers as a string
* foreach ($message->getHeaders() as $name => $values) {
* echo $name . ": " . implode(", ", $values);
* }
*
* @return array Returns an associative array of the message's headers.
*/
public function getHeaders();
/**
* Retrieve a header by the given case-insensitive name.
*
* By default, this method returns all of the header values of the given
* case-insensitive header name as a string concatenated together using
* a comma. Because some header should not be concatenated together using a
* comma, this method provides a Boolean argument that can be used to
* retrieve the associated header values as an array of strings.
*
* @param string $header Case-insensitive header name.
* @param bool $asArray Set to true to retrieve the header value as an
* array of strings.
*
* @return array|string
*/
public function getHeader($header, $asArray = false);
/**
* Checks if a header exists by the given case-insensitive name.
*
* @param string $header Case-insensitive header name.
*
* @return bool Returns true if any header names match the given header
* name using a case-insensitive string comparison. Returns false if
* no matching header name is found in the message.
*/
public function hasHeader($header);
/**
* Remove a specific header by case-insensitive name.
*
* @param string $header Case-insensitive header name.
*
* @return self
*/
public function removeHeader($header);
/**
* Appends a header value to any existing values associated with the
* given header name.
*
* @param string $header Header name to add
* @param string $value Value of the header
*
* @return self
*/
public function addHeader($header, $value);
/**
* Merges in an associative array of headers.
*
* Each array key MUST be a string representing the case-insensitive name
* of a header. Each value MUST be either a string or an array of strings.
* For each value, the value is appended to any existing header of the same
* name, or, if a header does not already exist by the given name, then the
* header is added.
*
* @param array $headers Associative array of headers to add to the message
*
* @return self
*/
public function addHeaders(array $headers);
/**
* Sets a header, replacing any existing values of any headers with the
* same case-insensitive name.
*
* The header values MUST be a string or an array of strings.
*
* @param string $header Header name
* @param string|array $value Header value(s)
*
* @return self Returns the message.
*/
public function setHeader($header, $value);
/**
* Sets headers, replacing any headers that have already been set on the
* message.
*
* The array keys MUST be a string. The array values must be either a
* string or an array of strings.
*
* @param array $headers Headers to set.
*
* @return self Returns the message.
*/
public function setHeaders(array $headers);
}

View file

@ -0,0 +1,172 @@
<?php
namespace GuzzleHttp\Message;
/**
* Request and response parser used by Guzzle
*/
class MessageParser
{
/**
* Parse an HTTP request message into an associative array of parts.
*
* @param string $message HTTP request to parse
*
* @return array|bool Returns false if the message is invalid
*/
public function parseRequest($message)
{
if (!($parts = $this->parseMessage($message))) {
return false;
}
// Parse the protocol and protocol version
if (isset($parts['start_line'][2])) {
$startParts = explode('/', $parts['start_line'][2]);
$protocol = strtoupper($startParts[0]);
$version = isset($startParts[1]) ? $startParts[1] : '1.1';
} else {
$protocol = 'HTTP';
$version = '1.1';
}
$parsed = [
'method' => strtoupper($parts['start_line'][0]),
'protocol' => $protocol,
'protocol_version' => $version,
'headers' => $parts['headers'],
'body' => $parts['body']
];
$parsed['request_url'] = $this->getUrlPartsFromMessage(
(isset($parts['start_line'][1]) ? $parts['start_line'][1] : ''), $parsed);
return $parsed;
}
/**
* Parse an HTTP response message into an associative array of parts.
*
* @param string $message HTTP response to parse
*
* @return array|bool Returns false if the message is invalid
*/
public function parseResponse($message)
{
if (!($parts = $this->parseMessage($message))) {
return false;
}
list($protocol, $version) = explode('/', trim($parts['start_line'][0]));
return [
'protocol' => $protocol,
'protocol_version' => $version,
'code' => $parts['start_line'][1],
'reason_phrase' => isset($parts['start_line'][2]) ? $parts['start_line'][2] : '',
'headers' => $parts['headers'],
'body' => $parts['body']
];
}
/**
* Parse a message into parts
*
* @param string $message Message to parse
*
* @return array|bool
*/
private function parseMessage($message)
{
if (!$message) {
return false;
}
$startLine = null;
$headers = [];
$body = '';
// Iterate over each line in the message, accounting for line endings
$lines = preg_split('/(\\r?\\n)/', $message, -1, PREG_SPLIT_DELIM_CAPTURE);
for ($i = 0, $totalLines = count($lines); $i < $totalLines; $i += 2) {
$line = $lines[$i];
// If two line breaks were encountered, then this is the end of body
if (empty($line)) {
if ($i < $totalLines - 1) {
$body = implode('', array_slice($lines, $i + 2));
}
break;
}
// Parse message headers
if (!$startLine) {
$startLine = explode(' ', $line, 3);
} elseif (strpos($line, ':')) {
$parts = explode(':', $line, 2);
$key = trim($parts[0]);
$value = isset($parts[1]) ? trim($parts[1]) : '';
if (!isset($headers[$key])) {
$headers[$key] = $value;
} elseif (!is_array($headers[$key])) {
$headers[$key] = [$headers[$key], $value];
} else {
$headers[$key][] = $value;
}
}
}
return [
'start_line' => $startLine,
'headers' => $headers,
'body' => $body
];
}
/**
* Create URL parts from HTTP message parts
*
* @param string $requestUrl Associated URL
* @param array $parts HTTP message parts
*
* @return array
*/
private function getUrlPartsFromMessage($requestUrl, array $parts)
{
// Parse the URL information from the message
$urlParts = ['path' => $requestUrl, 'scheme' => 'http'];
// Check for the Host header
if (isset($parts['headers']['Host'])) {
$urlParts['host'] = $parts['headers']['Host'];
} elseif (isset($parts['headers']['host'])) {
$urlParts['host'] = $parts['headers']['host'];
} else {
$urlParts['host'] = null;
}
if (false === strpos($urlParts['host'], ':')) {
$urlParts['port'] = '';
} else {
$hostParts = explode(':', $urlParts['host']);
$urlParts['host'] = trim($hostParts[0]);
$urlParts['port'] = (int) trim($hostParts[1]);
if ($urlParts['port'] == 443) {
$urlParts['scheme'] = 'https';
}
}
// Check if a query is present
$path = $urlParts['path'];
$qpos = strpos($path, '?');
if ($qpos) {
$urlParts['query'] = substr($path, $qpos + 1);
$urlParts['path'] = substr($path, 0, $qpos);
} else {
$urlParts['query'] = '';
}
return $urlParts;
}
}

View file

@ -0,0 +1,216 @@
<?php
namespace GuzzleHttp\Message;
use GuzzleHttp\Event\HasEmitterTrait;
use GuzzleHttp\Collection;
use GuzzleHttp\Subscriber\Prepare;
use GuzzleHttp\Url;
/**
* HTTP request class to send requests
*/
class Request extends AbstractMessage implements RequestInterface
{
use HasEmitterTrait;
/** @var Url HTTP Url */
private $url;
/** @var string HTTP method */
private $method;
/** @var Collection Transfer options */
private $transferOptions;
/**
* @param string $method HTTP method
* @param string|Url $url HTTP URL to connect to. The URI scheme,
* host header, and URI are parsed from the full URL. If query string
* parameters are present they will be parsed as well.
* @param array|Collection $headers HTTP headers
* @param mixed $body Body to send with the request
* @param array $options Array of options to use with the request
* - emitter: Event emitter to use with the request
*/
public function __construct(
$method,
$url,
$headers = [],
$body = null,
array $options = []
) {
$this->setUrl($url);
$this->method = strtoupper($method);
$this->handleOptions($options);
$this->transferOptions = new Collection($options);
$this->addPrepareEvent();
if ($body !== null) {
$this->setBody($body);
}
if ($headers) {
foreach ($headers as $key => $value) {
$this->setHeader($key, $value);
}
}
}
public function __clone()
{
if ($this->emitter) {
$this->emitter = clone $this->emitter;
}
$this->transferOptions = clone $this->transferOptions;
$this->url = clone $this->url;
}
public function setUrl($url)
{
$this->url = $url instanceof Url ? $url : Url::fromString($url);
$this->updateHostHeaderFromUrl();
return $this;
}
public function getUrl()
{
return (string) $this->url;
}
public function setQuery($query)
{
$this->url->setQuery($query);
return $this;
}
public function getQuery()
{
return $this->url->getQuery();
}
public function setMethod($method)
{
$this->method = strtoupper($method);
return $this;
}
public function getMethod()
{
return $this->method;
}
public function getScheme()
{
return $this->url->getScheme();
}
public function setScheme($scheme)
{
$this->url->setScheme($scheme);
return $this;
}
public function getPort()
{
return $this->url->getPort();
}
public function setPort($port)
{
$this->url->setPort($port);
$this->updateHostHeaderFromUrl();
return $this;
}
public function getHost()
{
return $this->url->getHost();
}
public function setHost($host)
{
$this->url->setHost($host);
$this->updateHostHeaderFromUrl();
return $this;
}
public function getPath()
{
return '/' . ltrim($this->url->getPath(), '/');
}
public function setPath($path)
{
$this->url->setPath($path);
return $this;
}
public function getResource()
{
$resource = $this->getPath();
if ($query = (string) $this->url->getQuery()) {
$resource .= '?' . $query;
}
return $resource;
}
public function getConfig()
{
return $this->transferOptions;
}
protected function handleOptions(array &$options)
{
parent::handleOptions($options);
// Use a custom emitter if one is specified, and remove it from
// options that are exposed through getConfig()
if (isset($options['emitter'])) {
$this->emitter = $options['emitter'];
unset($options['emitter']);
}
}
protected function getStartLine()
{
return trim($this->method . ' ' . $this->getResource())
. ' HTTP/' . $this->getProtocolVersion();
}
/**
* Adds a subscriber that ensures a request's body is prepared before
* sending.
*/
private function addPrepareEvent()
{
static $subscriber;
if (!$subscriber) {
$subscriber = new Prepare();
}
$this->getEmitter()->attach($subscriber);
}
private function updateHostHeaderFromUrl()
{
$port = $this->url->getPort();
$scheme = $this->url->getScheme();
if ($host = $this->url->getHost()) {
if (($port == 80 && $scheme == 'http') ||
($port == 443 && $scheme == 'https')
) {
$this->setHeader('Host', $host);
} else {
$this->setHeader('Host', "{$host}:{$port}");
}
}
}
}

View file

@ -0,0 +1,150 @@
<?php
namespace GuzzleHttp\Message;
use GuzzleHttp\Event\HasEmitterInterface;
use GuzzleHttp\Query;
/**
* Generic HTTP request interface
*/
interface RequestInterface extends MessageInterface, HasEmitterInterface
{
/**
* Sets the request URL.
*
* The URL MUST be a string, or an object that implements the
* `__toString()` method.
*
* @param string $url Request URL.
*
* @return self Reference to the request.
* @throws \InvalidArgumentException If the URL is invalid.
*/
public function setUrl($url);
/**
* Gets the request URL as a string.
*
* @return string Returns the URL as a string.
*/
public function getUrl();
/**
* Get the resource part of the the request, including the path, query
* string, and fragment.
*
* @return string
*/
public function getResource();
/**
* Get the collection of key value pairs that will be used as the query
* string in the request.
*
* @return Query
*/
public function getQuery();
/**
* Set the query string used by the request
*
* @param array|Query $query Query to set
*
* @return self
*/
public function setQuery($query);
/**
* Get the HTTP method of the request.
*
* @return string
*/
public function getMethod();
/**
* Set the HTTP method of the request.
*
* @param string $method HTTP method
*
* @return self
*/
public function setMethod($method);
/**
* Get the URI scheme of the request (http, https, etc.).
*
* @return string
*/
public function getScheme();
/**
* Set the URI scheme of the request (http, https, etc.).
*
* @param string $scheme Scheme to set
*
* @return self
*/
public function setScheme($scheme);
/**
* Get the port scheme of the request (e.g., 80, 443, etc.).
*
* @return int
*/
public function getPort();
/**
* Set the port of the request.
*
* Setting a port modifies the Host header of a request as necessary.
*
* @param int $port Port to set
*
* @return self
*/
public function setPort($port);
/**
* Get the host of the request.
*
* @return string
*/
public function getHost();
/**
* Set the host of the request including an optional port.
*
* Including a port in the host argument will explicitly change the port of
* the request. If no port is found, the default port of the current
* request scheme will be utilized.
*
* @param string $host Host to set (e.g. www.yahoo.com, www.yahoo.com:80)
*
* @return self
*/
public function setHost($host);
/**
* Get the path of the request (e.g. '/', '/index.html').
*
* @return string
*/
public function getPath();
/**
* Set the path of the request (e.g. '/', '/index.html').
*
* @param string|array $path Path to set or array of segments to implode
*
* @return self
*/
public function setPath($path);
/**
* Get the request's configuration options.
*
* @return \GuzzleHttp\Collection
*/
public function getConfig();
}

View file

@ -0,0 +1,202 @@
<?php
namespace GuzzleHttp\Message;
use GuzzleHttp\Exception\ParseException;
use GuzzleHttp\Stream\StreamInterface;
/**
* Guzzle HTTP response object
*/
class Response extends AbstractMessage implements ResponseInterface
{
/** @var array Mapping of status codes to reason phrases */
private static $statusTexts = array(
100 => 'Continue',
101 => 'Switching Protocols',
102 => 'Processing',
200 => 'OK',
201 => 'Created',
202 => 'Accepted',
203 => 'Non-Authoritative Information',
204 => 'No Content',
205 => 'Reset Content',
206 => 'Partial Content',
207 => 'Multi-Status',
208 => 'Already Reported',
226 => 'IM Used',
300 => 'Multiple Choices',
301 => 'Moved Permanently',
302 => 'Found',
303 => 'See Other',
304 => 'Not Modified',
305 => 'Use Proxy',
307 => 'Temporary Redirect',
308 => 'Permanent Redirect',
400 => 'Bad Request',
401 => 'Unauthorized',
402 => 'Payment Required',
403 => 'Forbidden',
404 => 'Not Found',
405 => 'Method Not Allowed',
406 => 'Not Acceptable',
407 => 'Proxy Authentication Required',
408 => 'Request Timeout',
409 => 'Conflict',
410 => 'Gone',
411 => 'Length Required',
412 => 'Precondition Failed',
413 => 'Request Entity Too Large',
414 => 'Request-URI Too Long',
415 => 'Unsupported Media Type',
416 => 'Requested Range Not Satisfiable',
417 => 'Expectation Failed',
422 => 'Unprocessable Entity',
423 => 'Locked',
424 => 'Failed Dependency',
425 => 'Reserved for WebDAV advanced collections expired proposal',
426 => 'Upgrade required',
428 => 'Precondition Required',
429 => 'Too Many Requests',
431 => 'Request Header Fields Too Large',
500 => 'Internal Server Error',
501 => 'Not Implemented',
502 => 'Bad Gateway',
503 => 'Service Unavailable',
504 => 'Gateway Timeout',
505 => 'HTTP Version Not Supported',
506 => 'Variant Also Negotiates (Experimental)',
507 => 'Insufficient Storage',
508 => 'Loop Detected',
510 => 'Not Extended',
511 => 'Network Authentication Required',
);
/** @var string The reason phrase of the response (human readable code) */
private $reasonPhrase;
/** @var string The status code of the response */
private $statusCode;
/** @var string The effective URL that returned this response */
private $effectiveUrl;
/**
* @param string $statusCode The response status code (e.g. 200)
* @param array $headers The response headers
* @param StreamInterface $body The body of the response
* @param array $options Response message options
* - reason_phrase: Set a custom reason phrase
* - protocol_version: Set a custom protocol version
*/
public function __construct(
$statusCode,
array $headers = [],
StreamInterface $body = null,
array $options = []
) {
$this->statusCode = (string) $statusCode;
$this->handleOptions($options);
// Assume a reason phrase if one was not applied as an option
if (!$this->reasonPhrase &&
isset(self::$statusTexts[$this->statusCode])
) {
$this->reasonPhrase = self::$statusTexts[$this->statusCode];
}
if ($headers) {
$this->setHeaders($headers);
}
if ($body) {
$this->setBody($body);
}
}
public function getStatusCode()
{
return $this->statusCode;
}
public function getReasonPhrase()
{
return $this->reasonPhrase;
}
public function json(array $config = [])
{
try {
return \GuzzleHttp\json_decode(
(string) $this->getBody(),
isset($config['object']) ? !$config['object'] : true,
512,
isset($config['big_int_strings']) ? JSON_BIGINT_AS_STRING : 0
);
} catch (\InvalidArgumentException $e) {
throw new ParseException(
$e->getMessage(),
$this
);
}
}
public function xml(array $config = [])
{
$disableEntities = libxml_disable_entity_loader(true);
$internalErrors = libxml_use_internal_errors(true);
try {
// Allow XML to be retrieved even if there is no response body
$xml = new \SimpleXMLElement(
(string) $this->getBody() ?: '<root />',
LIBXML_NONET,
isset($config['ns']) ? $config['ns'] : '',
isset($config['ns_is_prefix']) ? $config['ns_is_prefix'] : false
);
libxml_disable_entity_loader($disableEntities);
libxml_use_internal_errors($internalErrors);
} catch (\Exception $e) {
libxml_disable_entity_loader($disableEntities);
libxml_use_internal_errors($internalErrors);
throw new ParseException(
'Unable to parse response body into XML: ' . $e->getMessage(),
$this
);
}
return $xml;
}
public function getEffectiveUrl()
{
return $this->effectiveUrl;
}
public function setEffectiveUrl($url)
{
$this->effectiveUrl = $url;
return $this;
}
/**
* Accepts and modifies the options provided to the response in the
* constructor.
*
* @param array $options Options array passed by reference.
*/
protected function handleOptions(array &$options = [])
{
parent::handleOptions($options);
if (isset($options['reason_phrase'])) {
$this->reasonPhrase = $options['reason_phrase'];
}
}
protected function getStartLine()
{
return 'HTTP/' . $this->getProtocolVersion()
. " {$this->statusCode} {$this->reasonPhrase}";
}
}

View file

@ -0,0 +1,86 @@
<?php
namespace GuzzleHttp\Message;
/**
* Represents an HTTP response message.
*/
interface ResponseInterface extends MessageInterface
{
/**
* Get the response status code (e.g. "200", "404", etc.)
*
* @return string
*/
public function getStatusCode();
/**
* Get the response reason phrase- a human readable version of the numeric
* status code
*
* @return string
*/
public function getReasonPhrase();
/**
* Get the effective URL that resulted in this response (e.g. the last
* redirect URL).
*
* @return string
*/
public function getEffectiveUrl();
/**
* Set the effective URL that resulted in this response (e.g. the last
* redirect URL).
*
* @param string $url Effective URL
*
* @return self
*/
public function setEffectiveUrl($url);
/**
* Parse the JSON response body and return the JSON decoded data.
*
* @param array $config Associative array of configuration settings used
* to control how the JSON data is parsed. Concrete implementations MAY
* add further configuration settings as needed, but they MUST implement
* functionality for the following options:
*
* - object: Set to true to parse JSON objects as PHP objects rather
* than associative arrays. Defaults to false.
* - big_int_strings: When set to true, large integers are converted to
* strings rather than floats. Defaults to false.
*
* Implementations are free to add further configuration settings as
* needed.
*
* @return mixed Returns the JSON decoded data based on the provided
* parse settings.
* @throws \RuntimeException if the response body is not in JSON format
*/
public function json(array $config = []);
/**
* Parse the XML response body and return a \SimpleXMLElement.
*
* In order to prevent XXE attacks, this method disables loading external
* entities. If you rely on external entities, then you must parse the
* XML response manually by accessing the response body directly.
*
* @param array $config Associative array of configuration settings used
* to control how the XML is parsed. Concrete implementations MAY add
* further configuration settings as needed, but they MUST implement
* functionality for the following options:
*
* - ns: Set to a string to represent the namespace prefix or URI
* - ns_is_prefix: Set to true to specify that the NS is a prefix rather
* than a URI (defaults to false).
*
* @return \SimpleXMLElement
* @throws \RuntimeException if the response body is not in XML format
* @link http://websec.io/2012/08/27/Preventing-XXE-in-PHP.html
*/
public function xml(array $config = []);
}

View file

@ -0,0 +1,964 @@
<?php
namespace GuzzleHttp;
/**
* Provides mappings of file extensions to mimetypes
* @link http://svn.apache.org/repos/asf/httpd/httpd/branches/1.3.x/conf/mime.types
*/
class Mimetypes
{
/** @var self */
protected static $instance;
/** @var array Mapping of extension to mimetype */
protected $mimetypes = array(
'3dml' => 'text/vnd.in3d.3dml',
'3g2' => 'video/3gpp2',
'3gp' => 'video/3gpp',
'7z' => 'application/x-7z-compressed',
'aab' => 'application/x-authorware-bin',
'aac' => 'audio/x-aac',
'aam' => 'application/x-authorware-map',
'aas' => 'application/x-authorware-seg',
'abw' => 'application/x-abiword',
'ac' => 'application/pkix-attr-cert',
'acc' => 'application/vnd.americandynamics.acc',
'ace' => 'application/x-ace-compressed',
'acu' => 'application/vnd.acucobol',
'acutc' => 'application/vnd.acucorp',
'adp' => 'audio/adpcm',
'aep' => 'application/vnd.audiograph',
'afm' => 'application/x-font-type1',
'afp' => 'application/vnd.ibm.modcap',
'ahead' => 'application/vnd.ahead.space',
'ai' => 'application/postscript',
'aif' => 'audio/x-aiff',
'aifc' => 'audio/x-aiff',
'aiff' => 'audio/x-aiff',
'air' => 'application/vnd.adobe.air-application-installer-package+zip',
'ait' => 'application/vnd.dvb.ait',
'ami' => 'application/vnd.amiga.ami',
'apk' => 'application/vnd.android.package-archive',
'application' => 'application/x-ms-application',
'apr' => 'application/vnd.lotus-approach',
'asa' => 'text/plain',
'asax' => 'application/octet-stream',
'asc' => 'application/pgp-signature',
'ascx' => 'text/plain',
'asf' => 'video/x-ms-asf',
'ashx' => 'text/plain',
'asm' => 'text/x-asm',
'asmx' => 'text/plain',
'aso' => 'application/vnd.accpac.simply.aso',
'asp' => 'text/plain',
'aspx' => 'text/plain',
'asx' => 'video/x-ms-asf',
'atc' => 'application/vnd.acucorp',
'atom' => 'application/atom+xml',
'atomcat' => 'application/atomcat+xml',
'atomsvc' => 'application/atomsvc+xml',
'atx' => 'application/vnd.antix.game-component',
'au' => 'audio/basic',
'avi' => 'video/x-msvideo',
'aw' => 'application/applixware',
'axd' => 'text/plain',
'azf' => 'application/vnd.airzip.filesecure.azf',
'azs' => 'application/vnd.airzip.filesecure.azs',
'azw' => 'application/vnd.amazon.ebook',
'bat' => 'application/x-msdownload',
'bcpio' => 'application/x-bcpio',
'bdf' => 'application/x-font-bdf',
'bdm' => 'application/vnd.syncml.dm+wbxml',
'bed' => 'application/vnd.realvnc.bed',
'bh2' => 'application/vnd.fujitsu.oasysprs',
'bin' => 'application/octet-stream',
'bmi' => 'application/vnd.bmi',
'bmp' => 'image/bmp',
'book' => 'application/vnd.framemaker',
'box' => 'application/vnd.previewsystems.box',
'boz' => 'application/x-bzip2',
'bpk' => 'application/octet-stream',
'btif' => 'image/prs.btif',
'bz' => 'application/x-bzip',
'bz2' => 'application/x-bzip2',
'c' => 'text/x-c',
'c11amc' => 'application/vnd.cluetrust.cartomobile-config',
'c11amz' => 'application/vnd.cluetrust.cartomobile-config-pkg',
'c4d' => 'application/vnd.clonk.c4group',
'c4f' => 'application/vnd.clonk.c4group',
'c4g' => 'application/vnd.clonk.c4group',
'c4p' => 'application/vnd.clonk.c4group',
'c4u' => 'application/vnd.clonk.c4group',
'cab' => 'application/vnd.ms-cab-compressed',
'car' => 'application/vnd.curl.car',
'cat' => 'application/vnd.ms-pki.seccat',
'cc' => 'text/x-c',
'cct' => 'application/x-director',
'ccxml' => 'application/ccxml+xml',
'cdbcmsg' => 'application/vnd.contact.cmsg',
'cdf' => 'application/x-netcdf',
'cdkey' => 'application/vnd.mediastation.cdkey',
'cdmia' => 'application/cdmi-capability',
'cdmic' => 'application/cdmi-container',
'cdmid' => 'application/cdmi-domain',
'cdmio' => 'application/cdmi-object',
'cdmiq' => 'application/cdmi-queue',
'cdx' => 'chemical/x-cdx',
'cdxml' => 'application/vnd.chemdraw+xml',
'cdy' => 'application/vnd.cinderella',
'cer' => 'application/pkix-cert',
'cfc' => 'application/x-coldfusion',
'cfm' => 'application/x-coldfusion',
'cgm' => 'image/cgm',
'chat' => 'application/x-chat',
'chm' => 'application/vnd.ms-htmlhelp',
'chrt' => 'application/vnd.kde.kchart',
'cif' => 'chemical/x-cif',
'cii' => 'application/vnd.anser-web-certificate-issue-initiation',
'cil' => 'application/vnd.ms-artgalry',
'cla' => 'application/vnd.claymore',
'class' => 'application/java-vm',
'clkk' => 'application/vnd.crick.clicker.keyboard',
'clkp' => 'application/vnd.crick.clicker.palette',
'clkt' => 'application/vnd.crick.clicker.template',
'clkw' => 'application/vnd.crick.clicker.wordbank',
'clkx' => 'application/vnd.crick.clicker',
'clp' => 'application/x-msclip',
'cmc' => 'application/vnd.cosmocaller',
'cmdf' => 'chemical/x-cmdf',
'cml' => 'chemical/x-cml',
'cmp' => 'application/vnd.yellowriver-custom-menu',
'cmx' => 'image/x-cmx',
'cod' => 'application/vnd.rim.cod',
'com' => 'application/x-msdownload',
'conf' => 'text/plain',
'cpio' => 'application/x-cpio',
'cpp' => 'text/x-c',
'cpt' => 'application/mac-compactpro',
'crd' => 'application/x-mscardfile',
'crl' => 'application/pkix-crl',
'crt' => 'application/x-x509-ca-cert',
'cryptonote' => 'application/vnd.rig.cryptonote',
'cs' => 'text/plain',
'csh' => 'application/x-csh',
'csml' => 'chemical/x-csml',
'csp' => 'application/vnd.commonspace',
'css' => 'text/css',
'cst' => 'application/x-director',
'csv' => 'text/csv',
'cu' => 'application/cu-seeme',
'curl' => 'text/vnd.curl',
'cww' => 'application/prs.cww',
'cxt' => 'application/x-director',
'cxx' => 'text/x-c',
'dae' => 'model/vnd.collada+xml',
'daf' => 'application/vnd.mobius.daf',
'dataless' => 'application/vnd.fdsn.seed',
'davmount' => 'application/davmount+xml',
'dcr' => 'application/x-director',
'dcurl' => 'text/vnd.curl.dcurl',
'dd2' => 'application/vnd.oma.dd2+xml',
'ddd' => 'application/vnd.fujixerox.ddd',
'deb' => 'application/x-debian-package',
'def' => 'text/plain',
'deploy' => 'application/octet-stream',
'der' => 'application/x-x509-ca-cert',
'dfac' => 'application/vnd.dreamfactory',
'dic' => 'text/x-c',
'dir' => 'application/x-director',
'dis' => 'application/vnd.mobius.dis',
'dist' => 'application/octet-stream',
'distz' => 'application/octet-stream',
'djv' => 'image/vnd.djvu',
'djvu' => 'image/vnd.djvu',
'dll' => 'application/x-msdownload',
'dmg' => 'application/octet-stream',
'dms' => 'application/octet-stream',
'dna' => 'application/vnd.dna',
'doc' => 'application/msword',
'docm' => 'application/vnd.ms-word.document.macroenabled.12',
'docx' => 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
'dot' => 'application/msword',
'dotm' => 'application/vnd.ms-word.template.macroenabled.12',
'dotx' => 'application/vnd.openxmlformats-officedocument.wordprocessingml.template',
'dp' => 'application/vnd.osgi.dp',
'dpg' => 'application/vnd.dpgraph',
'dra' => 'audio/vnd.dra',
'dsc' => 'text/prs.lines.tag',
'dssc' => 'application/dssc+der',
'dtb' => 'application/x-dtbook+xml',
'dtd' => 'application/xml-dtd',
'dts' => 'audio/vnd.dts',
'dtshd' => 'audio/vnd.dts.hd',
'dump' => 'application/octet-stream',
'dvi' => 'application/x-dvi',
'dwf' => 'model/vnd.dwf',
'dwg' => 'image/vnd.dwg',
'dxf' => 'image/vnd.dxf',
'dxp' => 'application/vnd.spotfire.dxp',
'dxr' => 'application/x-director',
'ecelp4800' => 'audio/vnd.nuera.ecelp4800',
'ecelp7470' => 'audio/vnd.nuera.ecelp7470',
'ecelp9600' => 'audio/vnd.nuera.ecelp9600',
'ecma' => 'application/ecmascript',
'edm' => 'application/vnd.novadigm.edm',
'edx' => 'application/vnd.novadigm.edx',
'efif' => 'application/vnd.picsel',
'ei6' => 'application/vnd.pg.osasli',
'elc' => 'application/octet-stream',
'eml' => 'message/rfc822',
'emma' => 'application/emma+xml',
'eol' => 'audio/vnd.digital-winds',
'eot' => 'application/vnd.ms-fontobject',
'eps' => 'application/postscript',
'epub' => 'application/epub+zip',
'es3' => 'application/vnd.eszigno3+xml',
'esf' => 'application/vnd.epson.esf',
'et3' => 'application/vnd.eszigno3+xml',
'etx' => 'text/x-setext',
'exe' => 'application/x-msdownload',
'exi' => 'application/exi',
'ext' => 'application/vnd.novadigm.ext',
'ez' => 'application/andrew-inset',
'ez2' => 'application/vnd.ezpix-album',
'ez3' => 'application/vnd.ezpix-package',
'f' => 'text/x-fortran',
'f4v' => 'video/x-f4v',
'f77' => 'text/x-fortran',
'f90' => 'text/x-fortran',
'fbs' => 'image/vnd.fastbidsheet',
'fcs' => 'application/vnd.isac.fcs',
'fdf' => 'application/vnd.fdf',
'fe_launch' => 'application/vnd.denovo.fcselayout-link',
'fg5' => 'application/vnd.fujitsu.oasysgp',
'fgd' => 'application/x-director',
'fh' => 'image/x-freehand',
'fh4' => 'image/x-freehand',
'fh5' => 'image/x-freehand',
'fh7' => 'image/x-freehand',
'fhc' => 'image/x-freehand',
'fig' => 'application/x-xfig',
'fli' => 'video/x-fli',
'flo' => 'application/vnd.micrografx.flo',
'flv' => 'video/x-flv',
'flw' => 'application/vnd.kde.kivio',
'flx' => 'text/vnd.fmi.flexstor',
'fly' => 'text/vnd.fly',
'fm' => 'application/vnd.framemaker',
'fnc' => 'application/vnd.frogans.fnc',
'for' => 'text/x-fortran',
'fpx' => 'image/vnd.fpx',
'frame' => 'application/vnd.framemaker',
'fsc' => 'application/vnd.fsc.weblaunch',
'fst' => 'image/vnd.fst',
'ftc' => 'application/vnd.fluxtime.clip',
'fti' => 'application/vnd.anser-web-funds-transfer-initiation',
'fvt' => 'video/vnd.fvt',
'fxp' => 'application/vnd.adobe.fxp',
'fxpl' => 'application/vnd.adobe.fxp',
'fzs' => 'application/vnd.fuzzysheet',
'g2w' => 'application/vnd.geoplan',
'g3' => 'image/g3fax',
'g3w' => 'application/vnd.geospace',
'gac' => 'application/vnd.groove-account',
'gdl' => 'model/vnd.gdl',
'geo' => 'application/vnd.dynageo',
'gex' => 'application/vnd.geometry-explorer',
'ggb' => 'application/vnd.geogebra.file',
'ggt' => 'application/vnd.geogebra.tool',
'ghf' => 'application/vnd.groove-help',
'gif' => 'image/gif',
'gim' => 'application/vnd.groove-identity-message',
'gmx' => 'application/vnd.gmx',
'gnumeric' => 'application/x-gnumeric',
'gph' => 'application/vnd.flographit',
'gqf' => 'application/vnd.grafeq',
'gqs' => 'application/vnd.grafeq',
'gram' => 'application/srgs',
'gre' => 'application/vnd.geometry-explorer',
'grv' => 'application/vnd.groove-injector',
'grxml' => 'application/srgs+xml',
'gsf' => 'application/x-font-ghostscript',
'gtar' => 'application/x-gtar',
'gtm' => 'application/vnd.groove-tool-message',
'gtw' => 'model/vnd.gtw',
'gv' => 'text/vnd.graphviz',
'gxt' => 'application/vnd.geonext',
'h' => 'text/x-c',
'h261' => 'video/h261',
'h263' => 'video/h263',
'h264' => 'video/h264',
'hal' => 'application/vnd.hal+xml',
'hbci' => 'application/vnd.hbci',
'hdf' => 'application/x-hdf',
'hh' => 'text/x-c',
'hlp' => 'application/winhlp',
'hpgl' => 'application/vnd.hp-hpgl',
'hpid' => 'application/vnd.hp-hpid',
'hps' => 'application/vnd.hp-hps',
'hqx' => 'application/mac-binhex40',
'hta' => 'application/octet-stream',
'htc' => 'text/html',
'htke' => 'application/vnd.kenameaapp',
'htm' => 'text/html',
'html' => 'text/html',
'hvd' => 'application/vnd.yamaha.hv-dic',
'hvp' => 'application/vnd.yamaha.hv-voice',
'hvs' => 'application/vnd.yamaha.hv-script',
'i2g' => 'application/vnd.intergeo',
'icc' => 'application/vnd.iccprofile',
'ice' => 'x-conference/x-cooltalk',
'icm' => 'application/vnd.iccprofile',
'ico' => 'image/x-icon',
'ics' => 'text/calendar',
'ief' => 'image/ief',
'ifb' => 'text/calendar',
'ifm' => 'application/vnd.shana.informed.formdata',
'iges' => 'model/iges',
'igl' => 'application/vnd.igloader',
'igm' => 'application/vnd.insors.igm',
'igs' => 'model/iges',
'igx' => 'application/vnd.micrografx.igx',
'iif' => 'application/vnd.shana.informed.interchange',
'imp' => 'application/vnd.accpac.simply.imp',
'ims' => 'application/vnd.ms-ims',
'in' => 'text/plain',
'ini' => 'text/plain',
'ipfix' => 'application/ipfix',
'ipk' => 'application/vnd.shana.informed.package',
'irm' => 'application/vnd.ibm.rights-management',
'irp' => 'application/vnd.irepository.package+xml',
'iso' => 'application/octet-stream',
'itp' => 'application/vnd.shana.informed.formtemplate',
'ivp' => 'application/vnd.immervision-ivp',
'ivu' => 'application/vnd.immervision-ivu',
'jad' => 'text/vnd.sun.j2me.app-descriptor',
'jam' => 'application/vnd.jam',
'jar' => 'application/java-archive',
'java' => 'text/x-java-source',
'jisp' => 'application/vnd.jisp',
'jlt' => 'application/vnd.hp-jlyt',
'jnlp' => 'application/x-java-jnlp-file',
'joda' => 'application/vnd.joost.joda-archive',
'jpe' => 'image/jpeg',
'jpeg' => 'image/jpeg',
'jpg' => 'image/jpeg',
'jpgm' => 'video/jpm',
'jpgv' => 'video/jpeg',
'jpm' => 'video/jpm',
'js' => 'text/javascript',
'json' => 'application/json',
'kar' => 'audio/midi',
'karbon' => 'application/vnd.kde.karbon',
'kfo' => 'application/vnd.kde.kformula',
'kia' => 'application/vnd.kidspiration',
'kml' => 'application/vnd.google-earth.kml+xml',
'kmz' => 'application/vnd.google-earth.kmz',
'kne' => 'application/vnd.kinar',
'knp' => 'application/vnd.kinar',
'kon' => 'application/vnd.kde.kontour',
'kpr' => 'application/vnd.kde.kpresenter',
'kpt' => 'application/vnd.kde.kpresenter',
'ksp' => 'application/vnd.kde.kspread',
'ktr' => 'application/vnd.kahootz',
'ktx' => 'image/ktx',
'ktz' => 'application/vnd.kahootz',
'kwd' => 'application/vnd.kde.kword',
'kwt' => 'application/vnd.kde.kword',
'lasxml' => 'application/vnd.las.las+xml',
'latex' => 'application/x-latex',
'lbd' => 'application/vnd.llamagraphics.life-balance.desktop',
'lbe' => 'application/vnd.llamagraphics.life-balance.exchange+xml',
'les' => 'application/vnd.hhe.lesson-player',
'lha' => 'application/octet-stream',
'link66' => 'application/vnd.route66.link66+xml',
'list' => 'text/plain',
'list3820' => 'application/vnd.ibm.modcap',
'listafp' => 'application/vnd.ibm.modcap',
'log' => 'text/plain',
'lostxml' => 'application/lost+xml',
'lrf' => 'application/octet-stream',
'lrm' => 'application/vnd.ms-lrm',
'ltf' => 'application/vnd.frogans.ltf',
'lvp' => 'audio/vnd.lucent.voice',
'lwp' => 'application/vnd.lotus-wordpro',
'lzh' => 'application/octet-stream',
'm13' => 'application/x-msmediaview',
'm14' => 'application/x-msmediaview',
'm1v' => 'video/mpeg',
'm21' => 'application/mp21',
'm2a' => 'audio/mpeg',
'm2v' => 'video/mpeg',
'm3a' => 'audio/mpeg',
'm3u' => 'audio/x-mpegurl',
'm3u8' => 'application/vnd.apple.mpegurl',
'm4a' => 'audio/mp4',
'm4u' => 'video/vnd.mpegurl',
'm4v' => 'video/mp4',
'ma' => 'application/mathematica',
'mads' => 'application/mads+xml',
'mag' => 'application/vnd.ecowin.chart',
'maker' => 'application/vnd.framemaker',
'man' => 'text/troff',
'mathml' => 'application/mathml+xml',
'mb' => 'application/mathematica',
'mbk' => 'application/vnd.mobius.mbk',
'mbox' => 'application/mbox',
'mc1' => 'application/vnd.medcalcdata',
'mcd' => 'application/vnd.mcd',
'mcurl' => 'text/vnd.curl.mcurl',
'mdb' => 'application/x-msaccess',
'mdi' => 'image/vnd.ms-modi',
'me' => 'text/troff',
'mesh' => 'model/mesh',
'meta4' => 'application/metalink4+xml',
'mets' => 'application/mets+xml',
'mfm' => 'application/vnd.mfmp',
'mgp' => 'application/vnd.osgeo.mapguide.package',
'mgz' => 'application/vnd.proteus.magazine',
'mid' => 'audio/midi',
'midi' => 'audio/midi',
'mif' => 'application/vnd.mif',
'mime' => 'message/rfc822',
'mj2' => 'video/mj2',
'mjp2' => 'video/mj2',
'mlp' => 'application/vnd.dolby.mlp',
'mmd' => 'application/vnd.chipnuts.karaoke-mmd',
'mmf' => 'application/vnd.smaf',
'mmr' => 'image/vnd.fujixerox.edmics-mmr',
'mny' => 'application/x-msmoney',
'mobi' => 'application/x-mobipocket-ebook',
'mods' => 'application/mods+xml',
'mov' => 'video/quicktime',
'movie' => 'video/x-sgi-movie',
'mp2' => 'audio/mpeg',
'mp21' => 'application/mp21',
'mp2a' => 'audio/mpeg',
'mp3' => 'audio/mpeg',
'mp4' => 'video/mp4',
'mp4a' => 'audio/mp4',
'mp4s' => 'application/mp4',
'mp4v' => 'video/mp4',
'mpc' => 'application/vnd.mophun.certificate',
'mpe' => 'video/mpeg',
'mpeg' => 'video/mpeg',
'mpg' => 'video/mpeg',
'mpg4' => 'video/mp4',
'mpga' => 'audio/mpeg',
'mpkg' => 'application/vnd.apple.installer+xml',
'mpm' => 'application/vnd.blueice.multipass',
'mpn' => 'application/vnd.mophun.application',
'mpp' => 'application/vnd.ms-project',
'mpt' => 'application/vnd.ms-project',
'mpy' => 'application/vnd.ibm.minipay',
'mqy' => 'application/vnd.mobius.mqy',
'mrc' => 'application/marc',
'mrcx' => 'application/marcxml+xml',
'ms' => 'text/troff',
'mscml' => 'application/mediaservercontrol+xml',
'mseed' => 'application/vnd.fdsn.mseed',
'mseq' => 'application/vnd.mseq',
'msf' => 'application/vnd.epson.msf',
'msh' => 'model/mesh',
'msi' => 'application/x-msdownload',
'msl' => 'application/vnd.mobius.msl',
'msty' => 'application/vnd.muvee.style',
'mts' => 'model/vnd.mts',
'mus' => 'application/vnd.musician',
'musicxml' => 'application/vnd.recordare.musicxml+xml',
'mvb' => 'application/x-msmediaview',
'mwf' => 'application/vnd.mfer',
'mxf' => 'application/mxf',
'mxl' => 'application/vnd.recordare.musicxml',
'mxml' => 'application/xv+xml',
'mxs' => 'application/vnd.triscape.mxs',
'mxu' => 'video/vnd.mpegurl',
'n-gage' => 'application/vnd.nokia.n-gage.symbian.install',
'n3' => 'text/n3',
'nb' => 'application/mathematica',
'nbp' => 'application/vnd.wolfram.player',
'nc' => 'application/x-netcdf',
'ncx' => 'application/x-dtbncx+xml',
'ngdat' => 'application/vnd.nokia.n-gage.data',
'nlu' => 'application/vnd.neurolanguage.nlu',
'nml' => 'application/vnd.enliven',
'nnd' => 'application/vnd.noblenet-directory',
'nns' => 'application/vnd.noblenet-sealer',
'nnw' => 'application/vnd.noblenet-web',
'npx' => 'image/vnd.net-fpx',
'nsf' => 'application/vnd.lotus-notes',
'oa2' => 'application/vnd.fujitsu.oasys2',
'oa3' => 'application/vnd.fujitsu.oasys3',
'oas' => 'application/vnd.fujitsu.oasys',
'obd' => 'application/x-msbinder',
'oda' => 'application/oda',
'odb' => 'application/vnd.oasis.opendocument.database',
'odc' => 'application/vnd.oasis.opendocument.chart',
'odf' => 'application/vnd.oasis.opendocument.formula',
'odft' => 'application/vnd.oasis.opendocument.formula-template',
'odg' => 'application/vnd.oasis.opendocument.graphics',
'odi' => 'application/vnd.oasis.opendocument.image',
'odm' => 'application/vnd.oasis.opendocument.text-master',
'odp' => 'application/vnd.oasis.opendocument.presentation',
'ods' => 'application/vnd.oasis.opendocument.spreadsheet',
'odt' => 'application/vnd.oasis.opendocument.text',
'oga' => 'audio/ogg',
'ogg' => 'audio/ogg',
'ogv' => 'video/ogg',
'ogx' => 'application/ogg',
'onepkg' => 'application/onenote',
'onetmp' => 'application/onenote',
'onetoc' => 'application/onenote',
'onetoc2' => 'application/onenote',
'opf' => 'application/oebps-package+xml',
'oprc' => 'application/vnd.palm',
'org' => 'application/vnd.lotus-organizer',
'osf' => 'application/vnd.yamaha.openscoreformat',
'osfpvg' => 'application/vnd.yamaha.openscoreformat.osfpvg+xml',
'otc' => 'application/vnd.oasis.opendocument.chart-template',
'otf' => 'application/x-font-otf',
'otg' => 'application/vnd.oasis.opendocument.graphics-template',
'oth' => 'application/vnd.oasis.opendocument.text-web',
'oti' => 'application/vnd.oasis.opendocument.image-template',
'otp' => 'application/vnd.oasis.opendocument.presentation-template',
'ots' => 'application/vnd.oasis.opendocument.spreadsheet-template',
'ott' => 'application/vnd.oasis.opendocument.text-template',
'oxt' => 'application/vnd.openofficeorg.extension',
'p' => 'text/x-pascal',
'p10' => 'application/pkcs10',
'p12' => 'application/x-pkcs12',
'p7b' => 'application/x-pkcs7-certificates',
'p7c' => 'application/pkcs7-mime',
'p7m' => 'application/pkcs7-mime',
'p7r' => 'application/x-pkcs7-certreqresp',
'p7s' => 'application/pkcs7-signature',
'p8' => 'application/pkcs8',
'pas' => 'text/x-pascal',
'paw' => 'application/vnd.pawaafile',
'pbd' => 'application/vnd.powerbuilder6',
'pbm' => 'image/x-portable-bitmap',
'pcf' => 'application/x-font-pcf',
'pcl' => 'application/vnd.hp-pcl',
'pclxl' => 'application/vnd.hp-pclxl',
'pct' => 'image/x-pict',
'pcurl' => 'application/vnd.curl.pcurl',
'pcx' => 'image/x-pcx',
'pdb' => 'application/vnd.palm',
'pdf' => 'application/pdf',
'pfa' => 'application/x-font-type1',
'pfb' => 'application/x-font-type1',
'pfm' => 'application/x-font-type1',
'pfr' => 'application/font-tdpfr',
'pfx' => 'application/x-pkcs12',
'pgm' => 'image/x-portable-graymap',
'pgn' => 'application/x-chess-pgn',
'pgp' => 'application/pgp-encrypted',
'php' => 'text/x-php',
'phps' => 'application/x-httpd-phps',
'pic' => 'image/x-pict',
'pkg' => 'application/octet-stream',
'pki' => 'application/pkixcmp',
'pkipath' => 'application/pkix-pkipath',
'plb' => 'application/vnd.3gpp.pic-bw-large',
'plc' => 'application/vnd.mobius.plc',
'plf' => 'application/vnd.pocketlearn',
'pls' => 'application/pls+xml',
'pml' => 'application/vnd.ctc-posml',
'png' => 'image/png',
'pnm' => 'image/x-portable-anymap',
'portpkg' => 'application/vnd.macports.portpkg',
'pot' => 'application/vnd.ms-powerpoint',
'potm' => 'application/vnd.ms-powerpoint.template.macroenabled.12',
'potx' => 'application/vnd.openxmlformats-officedocument.presentationml.template',
'ppam' => 'application/vnd.ms-powerpoint.addin.macroenabled.12',
'ppd' => 'application/vnd.cups-ppd',
'ppm' => 'image/x-portable-pixmap',
'pps' => 'application/vnd.ms-powerpoint',
'ppsm' => 'application/vnd.ms-powerpoint.slideshow.macroenabled.12',
'ppsx' => 'application/vnd.openxmlformats-officedocument.presentationml.slideshow',
'ppt' => 'application/vnd.ms-powerpoint',
'pptm' => 'application/vnd.ms-powerpoint.presentation.macroenabled.12',
'pptx' => 'application/vnd.openxmlformats-officedocument.presentationml.presentation',
'pqa' => 'application/vnd.palm',
'prc' => 'application/x-mobipocket-ebook',
'pre' => 'application/vnd.lotus-freelance',
'prf' => 'application/pics-rules',
'ps' => 'application/postscript',
'psb' => 'application/vnd.3gpp.pic-bw-small',
'psd' => 'image/vnd.adobe.photoshop',
'psf' => 'application/x-font-linux-psf',
'pskcxml' => 'application/pskc+xml',
'ptid' => 'application/vnd.pvi.ptid1',
'pub' => 'application/x-mspublisher',
'pvb' => 'application/vnd.3gpp.pic-bw-var',
'pwn' => 'application/vnd.3m.post-it-notes',
'pya' => 'audio/vnd.ms-playready.media.pya',
'pyv' => 'video/vnd.ms-playready.media.pyv',
'qam' => 'application/vnd.epson.quickanime',
'qbo' => 'application/vnd.intu.qbo',
'qfx' => 'application/vnd.intu.qfx',
'qps' => 'application/vnd.publishare-delta-tree',
'qt' => 'video/quicktime',
'qwd' => 'application/vnd.quark.quarkxpress',
'qwt' => 'application/vnd.quark.quarkxpress',
'qxb' => 'application/vnd.quark.quarkxpress',
'qxd' => 'application/vnd.quark.quarkxpress',
'qxl' => 'application/vnd.quark.quarkxpress',
'qxt' => 'application/vnd.quark.quarkxpress',
'ra' => 'audio/x-pn-realaudio',
'ram' => 'audio/x-pn-realaudio',
'rar' => 'application/x-rar-compressed',
'ras' => 'image/x-cmu-raster',
'rb' => 'text/plain',
'rcprofile' => 'application/vnd.ipunplugged.rcprofile',
'rdf' => 'application/rdf+xml',
'rdz' => 'application/vnd.data-vision.rdz',
'rep' => 'application/vnd.businessobjects',
'res' => 'application/x-dtbresource+xml',
'resx' => 'text/xml',
'rgb' => 'image/x-rgb',
'rif' => 'application/reginfo+xml',
'rip' => 'audio/vnd.rip',
'rl' => 'application/resource-lists+xml',
'rlc' => 'image/vnd.fujixerox.edmics-rlc',
'rld' => 'application/resource-lists-diff+xml',
'rm' => 'application/vnd.rn-realmedia',
'rmi' => 'audio/midi',
'rmp' => 'audio/x-pn-realaudio-plugin',
'rms' => 'application/vnd.jcp.javame.midlet-rms',
'rnc' => 'application/relax-ng-compact-syntax',
'roff' => 'text/troff',
'rp9' => 'application/vnd.cloanto.rp9',
'rpss' => 'application/vnd.nokia.radio-presets',
'rpst' => 'application/vnd.nokia.radio-preset',
'rq' => 'application/sparql-query',
'rs' => 'application/rls-services+xml',
'rsd' => 'application/rsd+xml',
'rss' => 'application/rss+xml',
'rtf' => 'application/rtf',
'rtx' => 'text/richtext',
's' => 'text/x-asm',
'saf' => 'application/vnd.yamaha.smaf-audio',
'sbml' => 'application/sbml+xml',
'sc' => 'application/vnd.ibm.secure-container',
'scd' => 'application/x-msschedule',
'scm' => 'application/vnd.lotus-screencam',
'scq' => 'application/scvp-cv-request',
'scs' => 'application/scvp-cv-response',
'scurl' => 'text/vnd.curl.scurl',
'sda' => 'application/vnd.stardivision.draw',
'sdc' => 'application/vnd.stardivision.calc',
'sdd' => 'application/vnd.stardivision.impress',
'sdkd' => 'application/vnd.solent.sdkm+xml',
'sdkm' => 'application/vnd.solent.sdkm+xml',
'sdp' => 'application/sdp',
'sdw' => 'application/vnd.stardivision.writer',
'see' => 'application/vnd.seemail',
'seed' => 'application/vnd.fdsn.seed',
'sema' => 'application/vnd.sema',
'semd' => 'application/vnd.semd',
'semf' => 'application/vnd.semf',
'ser' => 'application/java-serialized-object',
'setpay' => 'application/set-payment-initiation',
'setreg' => 'application/set-registration-initiation',
'sfd-hdstx' => 'application/vnd.hydrostatix.sof-data',
'sfs' => 'application/vnd.spotfire.sfs',
'sgl' => 'application/vnd.stardivision.writer-global',
'sgm' => 'text/sgml',
'sgml' => 'text/sgml',
'sh' => 'application/x-sh',
'shar' => 'application/x-shar',
'shf' => 'application/shf+xml',
'sig' => 'application/pgp-signature',
'silo' => 'model/mesh',
'sis' => 'application/vnd.symbian.install',
'sisx' => 'application/vnd.symbian.install',
'sit' => 'application/x-stuffit',
'sitx' => 'application/x-stuffitx',
'skd' => 'application/vnd.koan',
'skm' => 'application/vnd.koan',
'skp' => 'application/vnd.koan',
'skt' => 'application/vnd.koan',
'sldm' => 'application/vnd.ms-powerpoint.slide.macroenabled.12',
'sldx' => 'application/vnd.openxmlformats-officedocument.presentationml.slide',
'slt' => 'application/vnd.epson.salt',
'sm' => 'application/vnd.stepmania.stepchart',
'smf' => 'application/vnd.stardivision.math',
'smi' => 'application/smil+xml',
'smil' => 'application/smil+xml',
'snd' => 'audio/basic',
'snf' => 'application/x-font-snf',
'so' => 'application/octet-stream',
'spc' => 'application/x-pkcs7-certificates',
'spf' => 'application/vnd.yamaha.smaf-phrase',
'spl' => 'application/x-futuresplash',
'spot' => 'text/vnd.in3d.spot',
'spp' => 'application/scvp-vp-response',
'spq' => 'application/scvp-vp-request',
'spx' => 'audio/ogg',
'src' => 'application/x-wais-source',
'sru' => 'application/sru+xml',
'srx' => 'application/sparql-results+xml',
'sse' => 'application/vnd.kodak-descriptor',
'ssf' => 'application/vnd.epson.ssf',
'ssml' => 'application/ssml+xml',
'st' => 'application/vnd.sailingtracker.track',
'stc' => 'application/vnd.sun.xml.calc.template',
'std' => 'application/vnd.sun.xml.draw.template',
'stf' => 'application/vnd.wt.stf',
'sti' => 'application/vnd.sun.xml.impress.template',
'stk' => 'application/hyperstudio',
'stl' => 'application/vnd.ms-pki.stl',
'str' => 'application/vnd.pg.format',
'stw' => 'application/vnd.sun.xml.writer.template',
'sub' => 'image/vnd.dvb.subtitle',
'sus' => 'application/vnd.sus-calendar',
'susp' => 'application/vnd.sus-calendar',
'sv4cpio' => 'application/x-sv4cpio',
'sv4crc' => 'application/x-sv4crc',
'svc' => 'application/vnd.dvb.service',
'svd' => 'application/vnd.svd',
'svg' => 'image/svg+xml',
'svgz' => 'image/svg+xml',
'swa' => 'application/x-director',
'swf' => 'application/x-shockwave-flash',
'swi' => 'application/vnd.aristanetworks.swi',
'sxc' => 'application/vnd.sun.xml.calc',
'sxd' => 'application/vnd.sun.xml.draw',
'sxg' => 'application/vnd.sun.xml.writer.global',
'sxi' => 'application/vnd.sun.xml.impress',
'sxm' => 'application/vnd.sun.xml.math',
'sxw' => 'application/vnd.sun.xml.writer',
't' => 'text/troff',
'tao' => 'application/vnd.tao.intent-module-archive',
'tar' => 'application/x-tar',
'tcap' => 'application/vnd.3gpp2.tcap',
'tcl' => 'application/x-tcl',
'teacher' => 'application/vnd.smart.teacher',
'tei' => 'application/tei+xml',
'teicorpus' => 'application/tei+xml',
'tex' => 'application/x-tex',
'texi' => 'application/x-texinfo',
'texinfo' => 'application/x-texinfo',
'text' => 'text/plain',
'tfi' => 'application/thraud+xml',
'tfm' => 'application/x-tex-tfm',
'thmx' => 'application/vnd.ms-officetheme',
'tif' => 'image/tiff',
'tiff' => 'image/tiff',
'tmo' => 'application/vnd.tmobile-livetv',
'torrent' => 'application/x-bittorrent',
'tpl' => 'application/vnd.groove-tool-template',
'tpt' => 'application/vnd.trid.tpt',
'tr' => 'text/troff',
'tra' => 'application/vnd.trueapp',
'trm' => 'application/x-msterminal',
'tsd' => 'application/timestamped-data',
'tsv' => 'text/tab-separated-values',
'ttc' => 'application/x-font-ttf',
'ttf' => 'application/x-font-ttf',
'ttl' => 'text/turtle',
'twd' => 'application/vnd.simtech-mindmapper',
'twds' => 'application/vnd.simtech-mindmapper',
'txd' => 'application/vnd.genomatix.tuxedo',
'txf' => 'application/vnd.mobius.txf',
'txt' => 'text/plain',
'u32' => 'application/x-authorware-bin',
'udeb' => 'application/x-debian-package',
'ufd' => 'application/vnd.ufdl',
'ufdl' => 'application/vnd.ufdl',
'umj' => 'application/vnd.umajin',
'unityweb' => 'application/vnd.unity',
'uoml' => 'application/vnd.uoml+xml',
'uri' => 'text/uri-list',
'uris' => 'text/uri-list',
'urls' => 'text/uri-list',
'ustar' => 'application/x-ustar',
'utz' => 'application/vnd.uiq.theme',
'uu' => 'text/x-uuencode',
'uva' => 'audio/vnd.dece.audio',
'uvd' => 'application/vnd.dece.data',
'uvf' => 'application/vnd.dece.data',
'uvg' => 'image/vnd.dece.graphic',
'uvh' => 'video/vnd.dece.hd',
'uvi' => 'image/vnd.dece.graphic',
'uvm' => 'video/vnd.dece.mobile',
'uvp' => 'video/vnd.dece.pd',
'uvs' => 'video/vnd.dece.sd',
'uvt' => 'application/vnd.dece.ttml+xml',
'uvu' => 'video/vnd.uvvu.mp4',
'uvv' => 'video/vnd.dece.video',
'uvva' => 'audio/vnd.dece.audio',
'uvvd' => 'application/vnd.dece.data',
'uvvf' => 'application/vnd.dece.data',
'uvvg' => 'image/vnd.dece.graphic',
'uvvh' => 'video/vnd.dece.hd',
'uvvi' => 'image/vnd.dece.graphic',
'uvvm' => 'video/vnd.dece.mobile',
'uvvp' => 'video/vnd.dece.pd',
'uvvs' => 'video/vnd.dece.sd',
'uvvt' => 'application/vnd.dece.ttml+xml',
'uvvu' => 'video/vnd.uvvu.mp4',
'uvvv' => 'video/vnd.dece.video',
'uvvx' => 'application/vnd.dece.unspecified',
'uvx' => 'application/vnd.dece.unspecified',
'vcd' => 'application/x-cdlink',
'vcf' => 'text/x-vcard',
'vcg' => 'application/vnd.groove-vcard',
'vcs' => 'text/x-vcalendar',
'vcx' => 'application/vnd.vcx',
'vis' => 'application/vnd.visionary',
'viv' => 'video/vnd.vivo',
'vor' => 'application/vnd.stardivision.writer',
'vox' => 'application/x-authorware-bin',
'vrml' => 'model/vrml',
'vsd' => 'application/vnd.visio',
'vsf' => 'application/vnd.vsf',
'vss' => 'application/vnd.visio',
'vst' => 'application/vnd.visio',
'vsw' => 'application/vnd.visio',
'vtu' => 'model/vnd.vtu',
'vxml' => 'application/voicexml+xml',
'w3d' => 'application/x-director',
'wad' => 'application/x-doom',
'wav' => 'audio/x-wav',
'wax' => 'audio/x-ms-wax',
'wbmp' => 'image/vnd.wap.wbmp',
'wbs' => 'application/vnd.criticaltools.wbs+xml',
'wbxml' => 'application/vnd.wap.wbxml',
'wcm' => 'application/vnd.ms-works',
'wdb' => 'application/vnd.ms-works',
'weba' => 'audio/webm',
'webm' => 'video/webm',
'webp' => 'image/webp',
'wg' => 'application/vnd.pmi.widget',
'wgt' => 'application/widget',
'wks' => 'application/vnd.ms-works',
'wm' => 'video/x-ms-wm',
'wma' => 'audio/x-ms-wma',
'wmd' => 'application/x-ms-wmd',
'wmf' => 'application/x-msmetafile',
'wml' => 'text/vnd.wap.wml',
'wmlc' => 'application/vnd.wap.wmlc',
'wmls' => 'text/vnd.wap.wmlscript',
'wmlsc' => 'application/vnd.wap.wmlscriptc',
'wmv' => 'video/x-ms-wmv',
'wmx' => 'video/x-ms-wmx',
'wmz' => 'application/x-ms-wmz',
'woff' => 'application/x-font-woff',
'wpd' => 'application/vnd.wordperfect',
'wpl' => 'application/vnd.ms-wpl',
'wps' => 'application/vnd.ms-works',
'wqd' => 'application/vnd.wqd',
'wri' => 'application/x-mswrite',
'wrl' => 'model/vrml',
'wsdl' => 'application/wsdl+xml',
'wspolicy' => 'application/wspolicy+xml',
'wtb' => 'application/vnd.webturbo',
'wvx' => 'video/x-ms-wvx',
'x32' => 'application/x-authorware-bin',
'x3d' => 'application/vnd.hzn-3d-crossword',
'xap' => 'application/x-silverlight-app',
'xar' => 'application/vnd.xara',
'xbap' => 'application/x-ms-xbap',
'xbd' => 'application/vnd.fujixerox.docuworks.binder',
'xbm' => 'image/x-xbitmap',
'xdf' => 'application/xcap-diff+xml',
'xdm' => 'application/vnd.syncml.dm+xml',
'xdp' => 'application/vnd.adobe.xdp+xml',
'xdssc' => 'application/dssc+xml',
'xdw' => 'application/vnd.fujixerox.docuworks',
'xenc' => 'application/xenc+xml',
'xer' => 'application/patch-ops-error+xml',
'xfdf' => 'application/vnd.adobe.xfdf',
'xfdl' => 'application/vnd.xfdl',
'xht' => 'application/xhtml+xml',
'xhtml' => 'application/xhtml+xml',
'xhvml' => 'application/xv+xml',
'xif' => 'image/vnd.xiff',
'xla' => 'application/vnd.ms-excel',
'xlam' => 'application/vnd.ms-excel.addin.macroenabled.12',
'xlc' => 'application/vnd.ms-excel',
'xlm' => 'application/vnd.ms-excel',
'xls' => 'application/vnd.ms-excel',
'xlsb' => 'application/vnd.ms-excel.sheet.binary.macroenabled.12',
'xlsm' => 'application/vnd.ms-excel.sheet.macroenabled.12',
'xlsx' => 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
'xlt' => 'application/vnd.ms-excel',
'xltm' => 'application/vnd.ms-excel.template.macroenabled.12',
'xltx' => 'application/vnd.openxmlformats-officedocument.spreadsheetml.template',
'xlw' => 'application/vnd.ms-excel',
'xml' => 'application/xml',
'xo' => 'application/vnd.olpc-sugar',
'xop' => 'application/xop+xml',
'xpi' => 'application/x-xpinstall',
'xpm' => 'image/x-xpixmap',
'xpr' => 'application/vnd.is-xpr',
'xps' => 'application/vnd.ms-xpsdocument',
'xpw' => 'application/vnd.intercon.formnet',
'xpx' => 'application/vnd.intercon.formnet',
'xsl' => 'application/xml',
'xslt' => 'application/xslt+xml',
'xsm' => 'application/vnd.syncml+xml',
'xspf' => 'application/xspf+xml',
'xul' => 'application/vnd.mozilla.xul+xml',
'xvm' => 'application/xv+xml',
'xvml' => 'application/xv+xml',
'xwd' => 'image/x-xwindowdump',
'xyz' => 'chemical/x-xyz',
'yaml' => 'text/yaml',
'yang' => 'application/yang',
'yin' => 'application/yin+xml',
'yml' => 'text/yaml',
'zaz' => 'application/vnd.zzazz.deck+xml',
'zip' => 'application/zip',
'zir' => 'application/vnd.zul',
'zirz' => 'application/vnd.zul',
'zmm' => 'application/vnd.handheld-entertainment+xml'
);
/**
* Get a singleton instance of the class
*
* @return self
* @codeCoverageIgnore
*/
public static function getInstance()
{
if (!self::$instance) {
self::$instance = new self();
}
return self::$instance;
}
/**
* Get a mimetype value from a file extension
*
* @param string $extension File extension
*
* @return string|null
*
*/
public function fromExtension($extension)
{
$extension = strtolower($extension);
return isset($this->mimetypes[$extension])
? $this->mimetypes[$extension]
: null;
}
/**
* Get a mimetype from a filename
*
* @param string $filename Filename to generate a mimetype from
*
* @return string|null
*/
public function fromFilename($filename)
{
return $this->fromExtension(pathinfo($filename, PATHINFO_EXTENSION));
}
}

View file

@ -0,0 +1,292 @@
<?php
namespace GuzzleHttp\Post;
use GuzzleHttp\Stream;
/**
* Stream that when read returns bytes for a streaming multipart/form-data body
*/
class MultipartBody implements Stream\StreamInterface
{
/** @var Stream\StreamInterface */
private $files;
private $fields;
private $size;
private $buffer;
private $bufferedHeaders = [];
private $pos = 0;
private $currentFile = 0;
private $currentField = 0;
private $sentLast;
private $boundary;
/**
* @param array $fields Associative array of field names to values where
* each value is a string.
* @param array $files Associative array of PostFileInterface objects
* @param string $boundary You can optionally provide a specific boundary
* @throws \InvalidArgumentException
*/
public function __construct(
array $fields = [],
array $files = [],
$boundary = null
) {
$this->boundary = $boundary ?: uniqid();
$this->fields = $fields;
$this->files = $files;
// Ensure each file is a PostFileInterface
foreach ($this->files as $file) {
if (!$file instanceof PostFileInterface) {
throw new \InvalidArgumentException('All POST fields must '
. 'implement PostFieldInterface');
}
}
}
public function __toString()
{
$this->seek(0);
return $this->getContents();
}
public function getContents($maxLength = -1)
{
$buffer = '';
while (!$this->eof()) {
if ($maxLength === -1) {
$read = 1048576;
} else {
$len = strlen($buffer);
if ($len == $maxLength) {
break;
}
$read = min(1048576, $maxLength - $len);
}
$buffer .= $this->read($read);
}
return $buffer;
}
/**
* Get the boundary
*
* @return string
*/
public function getBoundary()
{
return $this->boundary;
}
public function close()
{
$this->detach();
}
public function detach()
{
$this->fields = $this->files = [];
}
/**
* The stream has reached an EOF when all of the fields and files have been
* read.
* {@inheritdoc}
*/
public function eof()
{
return $this->currentField == count($this->fields) &&
$this->currentFile == count($this->files);
}
public function tell()
{
return $this->pos;
}
public function isReadable()
{
return true;
}
public function isWritable()
{
return false;
}
/**
* The steam is seekable by default, but all attached files must be
* seekable too.
* {@inheritdoc}
*/
public function isSeekable()
{
foreach ($this->files as $file) {
if (!$file->getContent()->isSeekable()) {
return false;
}
}
return true;
}
public function getSize()
{
if ($this->size === null) {
foreach ($this->files as $file) {
// We must be able to ascertain the size of each attached file
if (null === ($size = $file->getContent()->getSize())) {
return null;
}
$this->size += strlen($this->getFileHeaders($file)) + $size;
}
foreach (array_keys($this->fields) as $key) {
$this->size += strlen($this->getFieldString($key));
}
$this->size += strlen("\r\n--{$this->boundary}--");
}
return $this->size;
}
public function read($length)
{
$content = '';
if ($this->buffer && !$this->buffer->eof()) {
$content .= $this->buffer->read($length);
}
if ($delta = $length - strlen($content)) {
$content .= $this->readData($delta);
}
if ($content === '' && !$this->sentLast) {
$this->sentLast = true;
$content = "\r\n--{$this->boundary}--";
}
return $content;
}
public function seek($offset, $whence = SEEK_SET)
{
if ($offset != 0 || $whence != SEEK_SET || !$this->isSeekable()) {
return false;
}
foreach ($this->files as $file) {
if (!$file->getContent()->seek(0)) {
throw new \RuntimeException('Rewind on multipart file failed '
. 'even though it shouldn\'t have');
}
}
$this->buffer = $this->sentLast = null;
$this->pos = $this->currentField = $this->currentFile = 0;
$this->bufferedHeaders = [];
return true;
}
public function write($string)
{
return false;
}
/**
* No data is in the read buffer, so more needs to be pulled in from fields
* and files.
*
* @param int $length Amount of data to read
*
* @return string
*/
private function readData($length)
{
$result = '';
if ($this->currentField < count($this->fields)) {
$result = $this->readField($length);
}
if ($result === '' && $this->currentFile < count($this->files)) {
$result = $this->readFile($length);
}
return $result;
}
/**
* Create a new stream buffer and inject form-data
*
* @param int $length Amount of data to read from the stream buffer
*
* @return string
*/
private function readField($length)
{
$name = array_keys($this->fields)[++$this->currentField - 1];
$this->buffer = Stream\create($this->getFieldString($name));
return $this->buffer->read($length);
}
/**
* Read data from a POST file, fill the read buffer with any overflow
*
* @param int $length Amount of data to read from the file
*
* @return string
*/
private function readFile($length)
{
$current = $this->files[$this->currentFile];
// Got to the next file and recursively return the read value, or bail
// if no more data can be read.
if ($current->getContent()->eof()) {
return ++$this->currentFile == count($this->files)
? ''
: $this->readFile($length);
}
// If this is the start of a file, then send the headers to the read
// buffer.
if (!isset($this->bufferedHeaders[$this->currentFile])) {
$this->buffer = Stream\create($this->getFileHeaders($current));
$this->bufferedHeaders[$this->currentFile] = true;
}
// More data needs to be read to meet the limit, so pull from the file
$content = $this->buffer ? $this->buffer->read($length) : '';
if (($remaining = $length - strlen($content)) > 0) {
$content .= $current->getContent()->read($remaining);
}
return $content;
}
private function getFieldString($key)
{
return sprintf(
"--%s\r\nContent-Disposition: form-data; name=\"%s\"\r\n\r\n%s\r\n",
$this->boundary,
$key,
$this->fields[$key]
);
}
private function getFileHeaders(PostFileInterface $file)
{
$headers = '';
foreach ($file->getHeaders() as $key => $value) {
$headers .= "{$key}: {$value}\r\n";
}
return "--{$this->boundary}\r\n" . trim($headers) . "\r\n\r\n";
}
}

View file

@ -0,0 +1,282 @@
<?php
namespace GuzzleHttp\Post;
use GuzzleHttp\Message\RequestInterface;
use GuzzleHttp\Stream;
use GuzzleHttp\Query;
/**
* Holds POST fields and files and creates a streaming body when read methods
* are called on the object.
*/
class PostBody implements PostBodyInterface
{
/** @var Stream\StreamInterface */
private $body;
/** @var callable */
private $aggregator;
private $fields = [];
/** @var PostFileInterface[] */
private $files = [];
private $forceMultipart = false;
/**
* Applies request headers to a request based on the POST state
*
* @param RequestInterface $request Request to update
*/
public function applyRequestHeaders(RequestInterface $request)
{
if ($this->files || $this->forceMultipart) {
$request->setHeader(
'Content-Type',
'multipart/form-data; boundary=' . $this->getBody()->getBoundary()
);
} elseif ($this->fields) {
$request->setHeader('Content-Type', 'application/x-www-form-urlencoded');
}
if ($size = $this->getSize()) {
$request->setHeader('Content-Length', $size);
}
}
public function forceMultipartUpload($force)
{
$this->forceMultipart = $force;
return $this;
}
public function setAggregator(callable $aggregator)
{
$this->aggregator = $aggregator;
}
public function setField($name, $value)
{
$this->fields[$name] = $value;
$this->mutate();
return $this;
}
public function replaceFields(array $fields)
{
$this->fields = $fields;
$this->mutate();
return $this;
}
public function getField($name)
{
return isset($this->fields[$name]) ? $this->fields[$name] : null;
}
public function removeField($name)
{
unset($this->fields[$name]);
$this->mutate();
return $this;
}
public function getFields($asString = false)
{
if (!$asString) {
return $this->fields;
}
return (string) (new Query($this->fields))
->setEncodingType(Query::RFC1738)
->setAggregator($this->getAggregator());
}
public function hasField($name)
{
return isset($this->fields[$name]);
}
public function getFile($name)
{
foreach ($this->files as $file) {
if ($file->getName() == $name) {
return $file;
}
}
return null;
}
public function getFiles()
{
return $this->files;
}
public function addFile(PostFileInterface $file)
{
$this->files[] = $file;
$this->mutate();
return $this;
}
public function clearFiles()
{
$this->files = [];
$this->mutate();
return $this;
}
/**
* Returns the numbers of fields + files
*
* @return int
*/
public function count()
{
return count($this->files) + count($this->fields);
}
public function __toString()
{
return (string) $this->getBody();
}
public function getContents($maxLength = -1)
{
return $this->getBody()->getContents();
}
public function close()
{
return $this->body ? $this->body->close() : true;
}
public function detach()
{
$this->body = null;
$this->fields = $this->files = [];
return $this;
}
public function eof()
{
return $this->getBody()->eof();
}
public function tell()
{
return $this->body ? $this->body->tell() : 0;
}
public function isSeekable()
{
return true;
}
public function isReadable()
{
return true;
}
public function isWritable()
{
return false;
}
public function getSize()
{
return $this->getBody()->getSize();
}
public function seek($offset, $whence = SEEK_SET)
{
return $this->getBody()->seek($offset, $whence);
}
public function read($length)
{
return $this->getBody()->read($length);
}
public function write($string)
{
return false;
}
/**
* Return a stream object that is built from the POST fields and files.
*
* If one has already been created, the previously created stream will be
* returned.
*/
private function getBody()
{
if ($this->body) {
return $this->body;
} elseif ($this->files || $this->forceMultipart) {
return $this->body = $this->createMultipart();
} elseif ($this->fields) {
return $this->body = $this->createUrlEncoded();
} else {
return $this->body = Stream\create();
}
}
/**
* Get the aggregator used to join multi-valued field parameters
*
* @return callable
*/
final protected function getAggregator()
{
if (!$this->aggregator) {
$this->aggregator = Query::phpAggregator();
}
return $this->aggregator;
}
/**
* Creates a multipart/form-data body stream
*
* @return MultipartBody
*/
private function createMultipart()
{
// Flatten the nested query string values using the correct aggregator
$query = (string) (new Query($this->fields))
->setEncodingType(false)
->setAggregator($this->getAggregator());
// Convert the flattened query string back into an array
$fields = Query::fromString($query)->toArray();
return new MultipartBody($fields, $this->files);
}
/**
* Creates an application/x-www-form-urlencoded stream body
*
* @return Stream\StreamInterface
*/
private function createUrlEncoded()
{
return Stream\create($this->getFields(true));
}
/**
* Get rid of any cached data
*/
private function mutate()
{
$this->body = null;
}
}

View file

@ -0,0 +1,129 @@
<?php
namespace GuzzleHttp\Post;
use GuzzleHttp\Message\RequestInterface;
use GuzzleHttp\Stream\StreamInterface;
/**
* Represents a POST body that is sent as either a multipart/form-data stream
* or application/x-www-urlencoded stream.
*/
interface PostBodyInterface extends StreamInterface, \Countable
{
/**
* Apply headers to the request appropriate for the current state of the object
*
* @param RequestInterface $request Request
*/
public function applyRequestHeaders(RequestInterface $request);
/**
* Set a specific field
*
* @param string $name Name of the field to set
* @param string|array $value Value to set
*
* @return $this
*/
public function setField($name, $value);
/**
* Set the aggregation strategy that will be used to turn multi-valued
* fields into a string.
*
* The aggregation function accepts a deeply nested array of query string
* values and returns a flattened associative array of key value pairs.
*
* @param callable $aggregator
*/
public function setAggregator(callable $aggregator);
/**
* Set to true to force a multipart upload even if there are no files.
*
* @param bool $force Set to true to force multipart uploads or false to
* remove this flag.
*
* @return self
*/
public function forceMultipartUpload($force);
/**
* Replace all existing form fields with an array of fields
*
* @param array $fields Associative array of fields to set
*
* @return $this
*/
public function replaceFields(array $fields);
/**
* Get a specific field by name
*
* @param string $name Name of the POST field to retrieve
*
* @return string|null
*/
public function getField($name);
/**
* Remove a field by name
*
* @param string $name Name of the field to remove
*
* @return $this
*/
public function removeField($name);
/**
* Returns an associative array of names to values or a query string.
*
* @param bool $asString Set to true to retrieve the fields as a query
* string.
*
* @return array|string
*/
public function getFields($asString = false);
/**
* Returns true if a field is set
*
* @param string $name Name of the field to set
*
* @return bool
*/
public function hasField($name);
/**
* Get all of the files
*
* @return array Returns an array of PostFileInterface objects
*/
public function getFiles();
/**
* Get a POST file by name.
*
* @param string $name Name of the POST file to retrieve
*
* @return PostFileInterface|null
*/
public function getFile($name);
/**
* Add a file to the POST
*
* @param PostFileInterface $file File to add
*
* @return $this
*/
public function addFile(PostFileInterface $file);
/**
* Remove all files from the collection
*
* @return $this
*/
public function clearFiles();
}

View file

@ -0,0 +1,138 @@
<?php
namespace GuzzleHttp\Post;
use GuzzleHttp\Mimetypes;
use GuzzleHttp\Stream\MetadataStreamInterface;
use GuzzleHttp\Stream\StreamInterface;
/**
* Post file upload
*/
class PostFile implements PostFileInterface
{
private $name;
private $filename;
private $content;
private $headers = [];
/**
* @param null $name Name of the form field
* @param mixed $content Data to send
* @param null $filename Filename content-disposition attribute
* @param array $headers Array of headers to set on the file
* (can override any default headers)
* @throws \RuntimeException when filename is not passed or can't be determined
*/
public function __construct(
$name,
$content,
$filename = null,
array $headers = []
) {
$this->headers = $headers;
$this->name = $name;
$this->prepareContent($content);
$this->prepareFilename($filename);
$this->prepareDefaultHeaders();
}
public function getName()
{
return $this->name;
}
public function getFilename()
{
return $this->filename;
}
public function getContent()
{
return $this->content;
}
public function getHeaders()
{
return $this->headers;
}
/**
* Prepares the contents of a POST file.
*
* @param mixed $content Content of the POST file
*/
private function prepareContent($content)
{
$this->content = $content;
if (!($this->content instanceof StreamInterface)) {
$this->content = \GuzzleHttp\Stream\create($this->content);
} elseif ($this->content instanceof MultipartBody) {
if (!$this->hasHeader('Content-Disposition')) {
$disposition = 'form-data; name="' . $this->name .'"';
$this->headers['Content-Disposition'] = $disposition;
}
if (!$this->hasHeader('Content-Type')) {
$this->headers['Content-Type'] = sprintf(
"multipart/form-data; boundary=%s",
$this->content->getBoundary()
);
}
}
}
/**
* Applies a file name to the POST file based on various checks.
*
* @param string|null $filename Filename to apply (or null to guess)
*/
private function prepareFilename($filename)
{
$this->filename = $filename;
if (!$this->filename &&
$this->content instanceof MetadataStreamInterface
) {
$this->filename = $this->content->getMetadata('uri');
}
if (!$this->filename || substr($this->filename, 0, 6) === 'php://') {
$this->filename = $this->name;
}
}
/**
* Applies default Content-Disposition and Content-Type headers if needed.
*/
private function prepareDefaultHeaders()
{
// Set a default content-disposition header if one was no provided
if (!$this->hasHeader('Content-Disposition')) {
$this->headers['Content-Disposition'] = sprintf(
'form-data; filename="%s"; name="%s"',
basename($this->filename),
$this->name
);
}
// Set a default Content-Type if one was not supplied
if (!$this->hasHeader('Content-Type')) {
$this->headers['Content-Type'] = Mimetypes::getInstance()
->fromFilename($this->filename) ?: 'text/plain';
}
}
/**
* Check if a specific header exists on the POST file by name.
*
* @param string $name Case-insensitive header to check
*
* @return bool
*/
private function hasHeader($name)
{
return isset(array_change_key_case($this->headers)[strtolower($name)]);
}
}

View file

@ -0,0 +1,42 @@
<?php
namespace GuzzleHttp\Post;
use GuzzleHttp\Stream\StreamInterface;
/**
* Post file upload interface
*/
interface PostFileInterface
{
/**
* Get the name of the form field
*
* @return string
*/
public function getName();
/**
* Get the full path to the file
*
* @return string
*/
public function getFilename();
/**
* Get the content
*
* @return StreamInterface
*/
public function getContent();
/**
* Gets all POST file headers.
*
* The keys represent the header name as it will be sent over the wire, and
* each value is a string.
*
* @return array Returns an associative array of the file's headers.
*/
public function getHeaders();
}

View file

@ -0,0 +1,226 @@
<?php
namespace GuzzleHttp;
/**
* Manages query string variables and can aggregate them into a string
*/
class Query extends Collection
{
const RFC3986 = 'RFC3986';
const RFC1738 = 'RFC1738';
/** @var bool URL encode fields and values */
private $encoding = self::RFC3986;
/** @var callable */
private $aggregator;
/**
* Parse a query string into a Query object
*
* @param string $query Query string to parse
*
* @return self
*/
public static function fromString($query)
{
$q = new static();
if ($query === '') {
return $q;
}
$foundDuplicates = $foundPhpStyle = false;
foreach (explode('&', $query) as $kvp) {
$parts = explode('=', $kvp, 2);
$key = rawurldecode($parts[0]);
if ($paramIsPhpStyleArray = substr($key, -2) == '[]') {
$foundPhpStyle = true;
$key = substr($key, 0, -2);
}
if (isset($parts[1])) {
$value = rawurldecode(str_replace('+', '%20', $parts[1]));
if (isset($q[$key])) {
$q->add($key, $value);
$foundDuplicates = true;
} elseif ($paramIsPhpStyleArray) {
$q[$key] = array($value);
} else {
$q[$key] = $value;
}
} else {
$q->add($key, null);
}
}
// Use the duplicate aggregator if duplicates were found and not using
// PHP style arrays.
if ($foundDuplicates && !$foundPhpStyle) {
$q->setAggregator(self::duplicateAggregator());
}
return $q;
}
/**
* Convert the query string parameters to a query string string
*
* @return string
*/
public function __toString()
{
if (!$this->data) {
return '';
}
// The default aggregator is statically cached
static $defaultAggregator;
if (!$this->aggregator) {
if (!$defaultAggregator) {
$defaultAggregator = self::phpAggregator();
}
$this->aggregator = $defaultAggregator;
}
$result = '';
$aggregator = $this->aggregator;
foreach ($aggregator($this->data) as $key => $values) {
foreach ($values as $value) {
if ($result) {
$result .= '&';
}
if ($this->encoding == self::RFC1738) {
$result .= urlencode($key);
if ($value !== null) {
$result .= '=' . urlencode($value);
}
} elseif ($this->encoding == self::RFC3986) {
$result .= rawurlencode($key);
if ($value !== null) {
$result .= '=' . rawurlencode($value);
}
} else {
$result .= $key;
if ($value !== null) {
$result .= '=' . $value;
}
}
}
}
return $result;
}
/**
* Controls how multi-valued query string parameters are aggregated into a
* string.
*
* $query->setAggregator($query::duplicateAggregator());
*
* @param callable $aggregator Callable used to convert a deeply nested
* array of query string variables into a flattened array of key value
* pairs. The callable accepts an array of query data and returns a
* flattened array of key value pairs where each value is an array of
* strings.
*
* @return self
*/
public function setAggregator(callable $aggregator)
{
$this->aggregator = $aggregator;
return $this;
}
/**
* Specify how values are URL encoded
*
* @param string|bool $type One of 'RFC1738', 'RFC3986', or false to disable encoding
*
* @return self
* @throws \InvalidArgumentException
*/
public function setEncodingType($type)
{
if ($type === false || $type === self::RFC1738 || $type === self::RFC3986) {
$this->encoding = $type;
} else {
throw new \InvalidArgumentException('Invalid URL encoding type');
}
return $this;
}
/**
* Query string aggregator that does not aggregate nested query string
* values and allows duplicates in the resulting array.
*
* Example: http://test.com?q=1&q=2
*
* @return callable
*/
public static function duplicateAggregator()
{
return function (array $data) {
return self::walkQuery($data, '', function ($key, $prefix) {
return is_int($key) ? $prefix : "{$prefix}[{$key}]";
});
};
}
/**
* Aggregates nested query string variables using the same technique as
* ``http_build_query()``.
*
* @param bool $numericIndices Pass false to not include numeric indices
* when multi-values query string parameters are present.
*
* @return callable
*/
public static function phpAggregator($numericIndices = true)
{
return function (array $data) use ($numericIndices) {
return self::walkQuery(
$data,
'',
function ($key, $prefix) use ($numericIndices) {
return !$numericIndices && is_int($key)
? "{$prefix}[]"
: "{$prefix}[{$key}]";
}
);
};
}
/**
* Easily create query aggregation functions by providing a key prefix
* function to this query string array walker.
*
* @param array $query Query string to walk
* @param string $keyPrefix Key prefix (start with '')
* @param callable $prefixer Function used to create a key prefix
*
* @return array
*/
public static function walkQuery(array $query, $keyPrefix, callable $prefixer)
{
$result = [];
foreach ($query as $key => $value) {
if ($keyPrefix) {
$key = $prefixer($key, $keyPrefix);
}
if (is_array($value)) {
$result += self::walkQuery($value, $key, $prefixer);
} elseif (isset($result[$key])) {
$result[$key][] = $value;
} else {
$result[$key] = array($value);
}
}
return $result;
}
}

View file

@ -0,0 +1,59 @@
<?php
namespace GuzzleHttp\Subscriber;
use GuzzleHttp\Event\RequestEvents;
use GuzzleHttp\Event\SubscriberInterface;
use GuzzleHttp\Event\CompleteEvent;
use GuzzleHttp\Event\BeforeEvent;
use GuzzleHttp\Cookie\CookieJar;
use GuzzleHttp\Cookie\CookieJarInterface;
/**
* Adds, extracts, and persists cookies between HTTP requests
*/
class Cookie implements SubscriberInterface
{
/** @var CookieJarInterface */
private $cookieJar;
/**
* @param CookieJarInterface $cookieJar Cookie jar used to hold cookies
*/
public function __construct(CookieJarInterface $cookieJar = null)
{
$this->cookieJar = $cookieJar ?: new CookieJar();
}
public function getEvents()
{
// Fire the cookie plugin complete event before redirecting
return [
'before' => ['onBefore'],
'complete' => ['onComplete', RequestEvents::REDIRECT_RESPONSE + 10]
];
}
/**
* Get the cookie cookieJar
*
* @return CookieJarInterface
*/
public function getCookieJar()
{
return $this->cookieJar;
}
public function onBefore(BeforeEvent $event)
{
$this->cookieJar->addCookieHeader($event->getRequest());
}
public function onComplete(CompleteEvent $event)
{
$this->cookieJar->extractCookies(
$event->getRequest(),
$event->getResponse()
);
}
}

Some files were not shown because too many files have changed in this diff Show more