mirror of
https://github.com/the-djmaze/snappymail.git
synced 2026-07-13 03:27:39 +03:00
Replaced Sabre\DAV
This commit is contained in:
parent
0127bdb2d8
commit
ebfc2a705b
21 changed files with 869 additions and 1305 deletions
|
|
@ -3,6 +3,7 @@
|
||||||
namespace RainLoop\Providers\AddressBook;
|
namespace RainLoop\Providers\AddressBook;
|
||||||
|
|
||||||
use \RainLoop\Providers\AddressBook\Enumerations\PropertyType;
|
use \RainLoop\Providers\AddressBook\Enumerations\PropertyType;
|
||||||
|
use \SnappyMail\DAV\Client as DAVClient;
|
||||||
|
|
||||||
class PdoAddressBook
|
class PdoAddressBook
|
||||||
extends \RainLoop\Common\PdoAbstract
|
extends \RainLoop\Common\PdoAbstract
|
||||||
|
|
@ -104,7 +105,7 @@ class PdoAddressBook
|
||||||
return $aResult;
|
return $aResult;
|
||||||
}
|
}
|
||||||
|
|
||||||
private function prepearRemoteSyncData($oClient, string $sPath)
|
private function prepearRemoteSyncData(DAVClient $oClient, string $sPath)
|
||||||
{
|
{
|
||||||
$mResult = false;
|
$mResult = false;
|
||||||
$aResponse = null;
|
$aResponse = null;
|
||||||
|
|
@ -175,7 +176,7 @@ class PdoAddressBook
|
||||||
return $mResult;
|
return $mResult;
|
||||||
}
|
}
|
||||||
|
|
||||||
private function davClientRequest($oClient, string $sCmd, string $sUrl, $mData = null) : ?array
|
private function davClientRequest(DAVClient $oClient, string $sCmd, string $sUrl, $mData = null) : ?array
|
||||||
{
|
{
|
||||||
\MailSo\Base\Utils::ResetTimeLimit();
|
\MailSo\Base\Utils::ResetTimeLimit();
|
||||||
|
|
||||||
|
|
@ -214,7 +215,7 @@ class PdoAddressBook
|
||||||
return $aResponse;
|
return $aResponse;
|
||||||
}
|
}
|
||||||
|
|
||||||
private function detectionPropFind(\Sabre\DAV\Client $oClient, string $sPath) : ?array
|
private function detectionPropFind(DAVClient $oClient, string $sPath) : ?array
|
||||||
{
|
{
|
||||||
$aResponse = null;
|
$aResponse = null;
|
||||||
|
|
||||||
|
|
@ -239,7 +240,7 @@ class PdoAddressBook
|
||||||
return $aResponse;
|
return $aResponse;
|
||||||
}
|
}
|
||||||
|
|
||||||
private function getContactsPaths(\Sabre\DAV\Client $oClient, string $sUser, string $sPassword, string $sProxy = '') : array
|
private function getContactsPaths(DAVClient $oClient, string $sUser, string $sPassword, string $sProxy = '') : array
|
||||||
{
|
{
|
||||||
$aContactsPaths = array();
|
$aContactsPaths = array();
|
||||||
|
|
||||||
|
|
@ -249,11 +250,6 @@ class PdoAddressBook
|
||||||
// [{DAV:}current-user-principal] => /cloud/remote.php/carddav/principals/admin/
|
// [{DAV:}current-user-principal] => /cloud/remote.php/carddav/principals/admin/
|
||||||
// [{urn:ietf:params:xml:ns:carddav}addressbook-home-set] => /cloud/remote.php/carddav/addressbooks/admin/
|
// [{urn:ietf:params:xml:ns:carddav}addressbook-home-set] => /cloud/remote.php/carddav/addressbooks/admin/
|
||||||
|
|
||||||
if (!$oClient)
|
|
||||||
{
|
|
||||||
return $aContactsPaths;
|
|
||||||
}
|
|
||||||
|
|
||||||
$aResponse = $this->detectionPropFind($oClient, '/.well-known/carddav');
|
$aResponse = $this->detectionPropFind($oClient, '/.well-known/carddav');
|
||||||
|
|
||||||
$sNextPath = '';
|
$sNextPath = '';
|
||||||
|
|
@ -425,7 +421,7 @@ class PdoAddressBook
|
||||||
return $aContactsPaths;
|
return $aContactsPaths;
|
||||||
}
|
}
|
||||||
|
|
||||||
private function checkContactsPath(\Sabre\DAV\Client $oClient, string $sPath) : bool
|
private function checkContactsPath(DAVClient $oClient, string $sPath) : bool
|
||||||
{
|
{
|
||||||
if (!$oClient)
|
if (!$oClient)
|
||||||
{
|
{
|
||||||
|
|
@ -475,7 +471,7 @@ class PdoAddressBook
|
||||||
return $bGood;
|
return $bGood;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function getDavClientFromUrl(string $sUrl, string $sUser, string $sPassword, string $sProxy = '') : \Sabre\DAV\Client
|
public function getDavClientFromUrl(string $sUrl, string $sUser, string $sPassword, string $sProxy = '') : DAVClient
|
||||||
{
|
{
|
||||||
if (!\preg_match('/^http[s]?:\/\//i', $sUrl))
|
if (!\preg_match('/^http[s]?:\/\//i', $sUrl))
|
||||||
{
|
{
|
||||||
|
|
@ -509,7 +505,7 @@ class PdoAddressBook
|
||||||
$aSettings['proxy'] = $sProxy;
|
$aSettings['proxy'] = $sProxy;
|
||||||
}
|
}
|
||||||
|
|
||||||
$oClient = new \Sabre\DAV\Client($aSettings);
|
$oClient = new DAVClient($aSettings);
|
||||||
$oClient->setVerifyPeer(false);
|
$oClient->setVerifyPeer(false);
|
||||||
|
|
||||||
$oClient->__UrlPath__ = $aUrl['path'];
|
$oClient->__UrlPath__ = $aUrl['path'];
|
||||||
|
|
@ -519,13 +515,8 @@ class PdoAddressBook
|
||||||
return $oClient;
|
return $oClient;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function getDavClient(string $sUrl, string $sUser, string $sPassword, string $sProxy = '') : ?\Sabre\DAV\Client
|
public function getDavClient(string $sUrl, string $sUser, string $sPassword, string $sProxy = '') : ?DAVClient
|
||||||
{
|
{
|
||||||
if (!\class_exists('Sabre\DAV\Client'))
|
|
||||||
{
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
$aMatch = array();
|
$aMatch = array();
|
||||||
$sUserAddressBookNameName = '';
|
$sUserAddressBookNameName = '';
|
||||||
|
|
||||||
|
|
@ -538,10 +529,6 @@ class PdoAddressBook
|
||||||
}
|
}
|
||||||
|
|
||||||
$oClient = $this->getDavClientFromUrl($sUrl, $sUser, $sPassword, $sProxy);
|
$oClient = $this->getDavClientFromUrl($sUrl, $sUser, $sPassword, $sProxy);
|
||||||
if (!$oClient)
|
|
||||||
{
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
$sPath = $oClient->__UrlPath__;
|
$sPath = $oClient->__UrlPath__;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,656 +0,0 @@
|
||||||
<?php
|
|
||||||
|
|
||||||
namespace Sabre\DAV;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* SabreDAV DAV client
|
|
||||||
*
|
|
||||||
* This client wraps around Curl to provide a convenient API to a WebDAV
|
|
||||||
* server.
|
|
||||||
*
|
|
||||||
* NOTE: This class is experimental, it's api will likely change in the future.
|
|
||||||
*
|
|
||||||
* @copyright Copyright (C) 2007-2015 fruux GmbH (https://fruux.com/).
|
|
||||||
* @author Evert Pot (http://evertpot.com/)
|
|
||||||
* @license http://sabre.io/license/ Modified BSD License
|
|
||||||
*/
|
|
||||||
class Client {
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The propertyMap is a key-value array.
|
|
||||||
*
|
|
||||||
* If you use the propertyMap, any {DAV:}multistatus responses with the
|
|
||||||
* proeprties listed in this array, will automatically be mapped to a
|
|
||||||
* respective class.
|
|
||||||
*
|
|
||||||
* The {DAV:}resourcetype property is automatically added. This maps to
|
|
||||||
* Sabre\DAV\Property\ResourceType
|
|
||||||
*
|
|
||||||
* @var array
|
|
||||||
*/
|
|
||||||
public $propertyMap = array();
|
|
||||||
|
|
||||||
protected $baseUri;
|
|
||||||
protected $userName;
|
|
||||||
protected $password;
|
|
||||||
protected $proxy;
|
|
||||||
protected $trustedCertificates;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Basic authentication
|
|
||||||
*/
|
|
||||||
const AUTH_BASIC = 1;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Digest authentication
|
|
||||||
*/
|
|
||||||
const AUTH_DIGEST = 2;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The authentication type we're using.
|
|
||||||
*
|
|
||||||
* This is a bitmask of AUTH_BASIC and AUTH_DIGEST.
|
|
||||||
*
|
|
||||||
* If DIGEST is used, the client makes 1 extra request per request, to get
|
|
||||||
* the authentication tokens.
|
|
||||||
*
|
|
||||||
* @var int
|
|
||||||
*/
|
|
||||||
protected $authType;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Indicates if SSL verification is enabled or not.
|
|
||||||
*
|
|
||||||
* @var boolean
|
|
||||||
*/
|
|
||||||
protected $verifyPeer;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Constructor
|
|
||||||
*
|
|
||||||
* Settings are provided through the 'settings' argument. The following
|
|
||||||
* settings are supported:
|
|
||||||
*
|
|
||||||
* * baseUri
|
|
||||||
* * userName (optional)
|
|
||||||
* * password (optional)
|
|
||||||
* * proxy (optional)
|
|
||||||
*
|
|
||||||
* @param array $settings
|
|
||||||
*/
|
|
||||||
public function __construct(array $settings) {
|
|
||||||
|
|
||||||
if (!isset($settings['baseUri'])) {
|
|
||||||
throw new \InvalidArgumentException('A baseUri must be provided');
|
|
||||||
}
|
|
||||||
|
|
||||||
$validSettings = array(
|
|
||||||
'baseUri',
|
|
||||||
'userName',
|
|
||||||
'password',
|
|
||||||
'proxy',
|
|
||||||
);
|
|
||||||
|
|
||||||
foreach($validSettings as $validSetting) {
|
|
||||||
if (isset($settings[$validSetting])) {
|
|
||||||
$this->$validSetting = $settings[$validSetting];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isset($settings['authType'])) {
|
|
||||||
$this->authType = $settings['authType'];
|
|
||||||
} else {
|
|
||||||
$this->authType = self::AUTH_BASIC | self::AUTH_DIGEST;
|
|
||||||
}
|
|
||||||
|
|
||||||
$this->propertyMap['{DAV:}resourcetype'] = 'Sabre\\DAV\\Property\\ResourceType';
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Add trusted root certificates to the webdav client.
|
|
||||||
*
|
|
||||||
* The parameter certificates should be a absolute path to a file
|
|
||||||
* which contains all trusted certificates
|
|
||||||
*
|
|
||||||
* @param string $certificates
|
|
||||||
*/
|
|
||||||
public function addTrustedCertificates($certificates) {
|
|
||||||
$this->trustedCertificates = $certificates;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Enables/disables SSL peer verification
|
|
||||||
*
|
|
||||||
* @param boolean $value
|
|
||||||
*/
|
|
||||||
public function setVerifyPeer($value) {
|
|
||||||
$this->verifyPeer = $value;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Does a PROPFIND request
|
|
||||||
*
|
|
||||||
* The list of requested properties must be specified as an array, in clark
|
|
||||||
* notation.
|
|
||||||
*
|
|
||||||
* The returned array will contain a list of filenames as keys, and
|
|
||||||
* properties as values.
|
|
||||||
*
|
|
||||||
* The properties array will contain the list of properties. Only properties
|
|
||||||
* that are actually returned from the server (without error) will be
|
|
||||||
* returned, anything else is discarded.
|
|
||||||
*
|
|
||||||
* Depth should be either 0 or 1. A depth of 1 will cause a request to be
|
|
||||||
* made to the server to also return all child resources.
|
|
||||||
*
|
|
||||||
* @param string $url
|
|
||||||
* @param array $properties
|
|
||||||
* @param int $depth
|
|
||||||
* @return array
|
|
||||||
*/
|
|
||||||
public function propFind($url, array $properties, $depth = 0) {
|
|
||||||
|
|
||||||
$body = '<?xml version="1.0"?>' . "\n";
|
|
||||||
$body.= '<d:propfind xmlns:d="DAV:">' . "\n";
|
|
||||||
$body.= ' <d:prop>' . "\n";
|
|
||||||
|
|
||||||
foreach($properties as $property) {
|
|
||||||
|
|
||||||
list(
|
|
||||||
$namespace,
|
|
||||||
$elementName
|
|
||||||
) = XMLUtil::parseClarkNotation($property);
|
|
||||||
|
|
||||||
if ($namespace === 'DAV:') {
|
|
||||||
$body.=' <d:' . $elementName . ' />' . "\n";
|
|
||||||
} else {
|
|
||||||
$body.=" <x:" . $elementName . " xmlns:x=\"" . $namespace . "\"/>\n";
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
$body.= ' </d:prop>' . "\n";
|
|
||||||
$body.= '</d:propfind>';
|
|
||||||
|
|
||||||
$response = $this->request('PROPFIND', $url, $body, array(
|
|
||||||
'Depth' => $depth,
|
|
||||||
'Content-Type' => 'application/xml'
|
|
||||||
));
|
|
||||||
|
|
||||||
$result = $this->parseMultiStatus($response['body']);
|
|
||||||
|
|
||||||
// If depth was 0, we only return the top item
|
|
||||||
if ($depth===0) {
|
|
||||||
reset($result);
|
|
||||||
$result = current($result);
|
|
||||||
return isset($result[200])?$result[200]:array();
|
|
||||||
}
|
|
||||||
|
|
||||||
$newResult = array();
|
|
||||||
foreach($result as $href => $statusList) {
|
|
||||||
|
|
||||||
$newResult[$href] = isset($statusList[200])?$statusList[200]:array();
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
return $newResult;
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Updates a list of properties on the server
|
|
||||||
*
|
|
||||||
* The list of properties must have clark-notation properties for the keys,
|
|
||||||
* and the actual (string) value for the value. If the value is null, an
|
|
||||||
* attempt is made to delete the property.
|
|
||||||
*
|
|
||||||
* @todo Must be building the request using the DOM, and does not yet
|
|
||||||
* support complex properties.
|
|
||||||
* @param string $url
|
|
||||||
* @param array $properties
|
|
||||||
* @return void
|
|
||||||
*/
|
|
||||||
public function propPatch($url, array $properties) {
|
|
||||||
|
|
||||||
$body = '<?xml version="1.0"?>' . "\n";
|
|
||||||
$body.= '<d:propertyupdate xmlns:d="DAV:">' . "\n";
|
|
||||||
|
|
||||||
foreach($properties as $propName => $propValue) {
|
|
||||||
|
|
||||||
list(
|
|
||||||
$namespace,
|
|
||||||
$elementName
|
|
||||||
) = XMLUtil::parseClarkNotation($propName);
|
|
||||||
|
|
||||||
if ($propValue === null) {
|
|
||||||
|
|
||||||
$body.="<d:remove><d:prop>\n";
|
|
||||||
|
|
||||||
if ($namespace === 'DAV:') {
|
|
||||||
$body.=' <d:' . $elementName . ' />' . "\n";
|
|
||||||
} else {
|
|
||||||
$body.=" <x:" . $elementName . " xmlns:x=\"" . $namespace . "\"/>\n";
|
|
||||||
}
|
|
||||||
|
|
||||||
$body.="</d:prop></d:remove>\n";
|
|
||||||
|
|
||||||
} else {
|
|
||||||
|
|
||||||
$body.="<d:set><d:prop>\n";
|
|
||||||
if ($namespace === 'DAV:') {
|
|
||||||
$body.=' <d:' . $elementName . '>';
|
|
||||||
} else {
|
|
||||||
$body.=" <x:" . $elementName . " xmlns:x=\"" . $namespace . "\">";
|
|
||||||
}
|
|
||||||
// Shitty.. i know
|
|
||||||
$body.=htmlspecialchars($propValue, ENT_NOQUOTES, 'UTF-8');
|
|
||||||
if ($namespace === 'DAV:') {
|
|
||||||
$body.='</d:' . $elementName . '>' . "\n";
|
|
||||||
} else {
|
|
||||||
$body.="</x:" . $elementName . ">\n";
|
|
||||||
}
|
|
||||||
$body.="</d:prop></d:set>\n";
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
$body.= '</d:propertyupdate>';
|
|
||||||
|
|
||||||
$this->request('PROPPATCH', $url, $body, array(
|
|
||||||
'Content-Type' => 'application/xml'
|
|
||||||
));
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Performs an HTTP options request
|
|
||||||
*
|
|
||||||
* This method returns all the features from the 'DAV:' header as an array.
|
|
||||||
* If there was no DAV header, or no contents this method will return an
|
|
||||||
* empty array.
|
|
||||||
*
|
|
||||||
* @return array
|
|
||||||
*/
|
|
||||||
public function options() {
|
|
||||||
|
|
||||||
$result = $this->request('OPTIONS');
|
|
||||||
if (!isset($result['headers']['dav'])) {
|
|
||||||
return array();
|
|
||||||
}
|
|
||||||
|
|
||||||
$features = explode(',', $result['headers']['dav']);
|
|
||||||
foreach($features as &$v) {
|
|
||||||
$v = trim($v);
|
|
||||||
}
|
|
||||||
return $features;
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Performs an actual HTTP request, and returns the result.
|
|
||||||
*
|
|
||||||
* If the specified url is relative, it will be expanded based on the base
|
|
||||||
* url.
|
|
||||||
*
|
|
||||||
* The returned array contains 3 keys:
|
|
||||||
* * body - the response body
|
|
||||||
* * httpCode - a HTTP code (200, 404, etc)
|
|
||||||
* * headers - a list of response http headers. The header names have
|
|
||||||
* been lowercased.
|
|
||||||
*
|
|
||||||
* @param string $method
|
|
||||||
* @param string $url
|
|
||||||
* @param string $body
|
|
||||||
* @param array $headers
|
|
||||||
* @return array
|
|
||||||
*/
|
|
||||||
public function request($method, $url = '', $body = null, $headers = array()) {
|
|
||||||
|
|
||||||
$url = $this->getAbsoluteUrl($url);
|
|
||||||
|
|
||||||
$curlSettings = array(
|
|
||||||
CURLOPT_RETURNTRANSFER => true,
|
|
||||||
// Return headers as part of the response
|
|
||||||
CURLOPT_HEADER => true,
|
|
||||||
|
|
||||||
// For security we cast this to a string. If somehow an array could
|
|
||||||
// be passed here, it would be possible for an attacker to use @ to
|
|
||||||
// post local files.
|
|
||||||
CURLOPT_POSTFIELDS => (string)$body,
|
|
||||||
CURLOPT_USERAGENT => 'SnappyMail DAV Client', // TODO rainloop
|
|
||||||
// Automatically follow redirects
|
|
||||||
CURLOPT_FOLLOWLOCATION => true,
|
|
||||||
CURLOPT_MAXREDIRS => 5,
|
|
||||||
CURLOPT_PROTOCOLS => CURLPROTO_HTTP | CURLPROTO_HTTPS,
|
|
||||||
CURLOPT_REDIR_PROTOCOLS => CURLPROTO_HTTP | CURLPROTO_HTTPS,
|
|
||||||
);
|
|
||||||
|
|
||||||
if($this->verifyPeer !== null) {
|
|
||||||
$curlSettings[CURLOPT_SSL_VERIFYPEER] = $this->verifyPeer;
|
|
||||||
// TODO rainloop
|
|
||||||
if (!$this->verifyPeer) {
|
|
||||||
$curlSettings[CURLOPT_SSL_VERIFYHOST] = 0;
|
|
||||||
}
|
|
||||||
// END rainloop
|
|
||||||
}
|
|
||||||
|
|
||||||
if($this->trustedCertificates) {
|
|
||||||
$curlSettings[CURLOPT_CAINFO] = $this->trustedCertificates;
|
|
||||||
}
|
|
||||||
|
|
||||||
switch ($method) {
|
|
||||||
case 'HEAD' :
|
|
||||||
|
|
||||||
// do not read body with HEAD requests (this is necessary because cURL does not ignore the body with HEAD
|
|
||||||
// requests when the Content-Length header is given - which in turn is perfectly valid according to HTTP
|
|
||||||
// specs...) cURL does unfortunately return an error in this case ("transfer closed transfer closed with
|
|
||||||
// ... bytes remaining to read") this can be circumvented by explicitly telling cURL to ignore the
|
|
||||||
// response body
|
|
||||||
$curlSettings[CURLOPT_NOBODY] = true;
|
|
||||||
$curlSettings[CURLOPT_CUSTOMREQUEST] = 'HEAD';
|
|
||||||
break;
|
|
||||||
|
|
||||||
default:
|
|
||||||
$curlSettings[CURLOPT_CUSTOMREQUEST] = $method;
|
|
||||||
break;
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
// Adding HTTP headers
|
|
||||||
$nHeaders = array();
|
|
||||||
foreach($headers as $key=>$value) {
|
|
||||||
|
|
||||||
$nHeaders[] = $key . ': ' . $value;
|
|
||||||
|
|
||||||
}
|
|
||||||
$curlSettings[CURLOPT_HTTPHEADER] = $nHeaders;
|
|
||||||
|
|
||||||
if ($this->proxy) {
|
|
||||||
$curlSettings[CURLOPT_PROXY] = $this->proxy;
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($this->userName && $this->authType) {
|
|
||||||
$curlType = 0;
|
|
||||||
if ($this->authType & self::AUTH_BASIC) {
|
|
||||||
$curlType |= CURLAUTH_BASIC;
|
|
||||||
}
|
|
||||||
if ($this->authType & self::AUTH_DIGEST) {
|
|
||||||
$curlType |= CURLAUTH_DIGEST;
|
|
||||||
}
|
|
||||||
$curlSettings[CURLOPT_HTTPAUTH] = $curlType;
|
|
||||||
$curlSettings[CURLOPT_USERPWD] = $this->userName . ':' . $this->password;
|
|
||||||
}
|
|
||||||
|
|
||||||
list(
|
|
||||||
$response,
|
|
||||||
$curlInfo,
|
|
||||||
$curlErrNo,
|
|
||||||
$curlError
|
|
||||||
) = $this->curlRequest($url, $curlSettings);
|
|
||||||
|
|
||||||
$headerBlob = substr($response, 0, $curlInfo['header_size']);
|
|
||||||
$response = substr($response, $curlInfo['header_size']);
|
|
||||||
|
|
||||||
// In the case of 100 Continue, or redirects we'll have multiple lists
|
|
||||||
// of headers for each separate HTTP response. We can easily split this
|
|
||||||
// because they are separated by \r\n\r\n
|
|
||||||
$headerBlob = explode("\r\n\r\n", trim($headerBlob, "\r\n"));
|
|
||||||
|
|
||||||
// We only care about the last set of headers
|
|
||||||
$headerBlob = $headerBlob[count($headerBlob)-1];
|
|
||||||
|
|
||||||
// Splitting headers
|
|
||||||
$headerBlob = explode("\r\n", $headerBlob);
|
|
||||||
|
|
||||||
$headers = array();
|
|
||||||
foreach($headerBlob as $header) {
|
|
||||||
$parts = explode(':', $header, 2);
|
|
||||||
if (count($parts)==2) {
|
|
||||||
$headers[strtolower(trim($parts[0]))] = trim($parts[1]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
$response = array(
|
|
||||||
'body' => $response,
|
|
||||||
'statusCode' => $curlInfo['http_code'],
|
|
||||||
'headers' => $headers
|
|
||||||
);
|
|
||||||
|
|
||||||
if ($curlErrNo) {
|
|
||||||
throw new Exception('[CURL] Error while making request: ' . $curlError . ' (error code: ' . $curlErrNo . ')');
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($response['statusCode']>=400) {
|
|
||||||
switch ($response['statusCode']) {
|
|
||||||
case 400 :
|
|
||||||
throw new Exception\BadRequest('Bad request');
|
|
||||||
case 401 :
|
|
||||||
throw new Exception\NotAuthenticated('Not authenticated');
|
|
||||||
case 402 :
|
|
||||||
throw new Exception\PaymentRequired('Payment required');
|
|
||||||
case 403 :
|
|
||||||
throw new Exception\Forbidden('Forbidden');
|
|
||||||
case 404:
|
|
||||||
throw new Exception\NotFound('Resource not found.');
|
|
||||||
case 405 :
|
|
||||||
throw new Exception\MethodNotAllowed('Method not allowed');
|
|
||||||
case 409 :
|
|
||||||
throw new Exception\Conflict('Conflict');
|
|
||||||
case 412 :
|
|
||||||
throw new Exception\PreconditionFailed('Precondition failed');
|
|
||||||
case 416 :
|
|
||||||
throw new Exception\RequestedRangeNotSatisfiable('Requested Range Not Satisfiable');
|
|
||||||
case 500 :
|
|
||||||
throw new Exception('Internal server error');
|
|
||||||
case 501 :
|
|
||||||
throw new Exception\NotImplemented('Not Implemented');
|
|
||||||
case 507 :
|
|
||||||
throw new Exception\InsufficientStorage('Insufficient storage');
|
|
||||||
default:
|
|
||||||
throw new Exception('HTTP error response. (errorcode ' . $response['statusCode'] . ')');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return $response;
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Wrapper for all curl functions.
|
|
||||||
*
|
|
||||||
* The only reason this was split out in a separate method, is so it
|
|
||||||
* becomes easier to unittest.
|
|
||||||
*
|
|
||||||
* @param string $url
|
|
||||||
* @param array $settings
|
|
||||||
* @return array
|
|
||||||
*/
|
|
||||||
// @codeCoverageIgnoreStart
|
|
||||||
protected function curlRequest($url, $settings) {
|
|
||||||
|
|
||||||
// TODO rainloop
|
|
||||||
$curl = curl_init($url);
|
|
||||||
|
|
||||||
if (ini_get('open_basedir') === '')
|
|
||||||
{
|
|
||||||
curl_setopt_array($curl, $settings);
|
|
||||||
$data = curl_exec($curl);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
$settings[CURLOPT_FOLLOWLOCATION] = false;
|
|
||||||
curl_setopt_array($curl, $settings);
|
|
||||||
|
|
||||||
$max_redirects = isset($settings[CURLOPT_MAXREDIRS]) ? $settings[CURLOPT_MAXREDIRS] : 5;
|
|
||||||
$mr = $max_redirects;
|
|
||||||
if ($mr > 0)
|
|
||||||
{
|
|
||||||
$newurl = curl_getinfo($curl, CURLINFO_EFFECTIVE_URL);
|
|
||||||
|
|
||||||
$rcurl = curl_copy_handle($curl);
|
|
||||||
curl_setopt($rcurl, CURLOPT_HEADER, true);
|
|
||||||
curl_setopt($rcurl, CURLOPT_NOBODY, true);
|
|
||||||
curl_setopt($rcurl, CURLOPT_FORBID_REUSE, false);
|
|
||||||
curl_setopt($rcurl, CURLOPT_RETURNTRANSFER, true);
|
|
||||||
do
|
|
||||||
{
|
|
||||||
curl_setopt($rcurl, CURLOPT_URL, $newurl);
|
|
||||||
$header = curl_exec($rcurl);
|
|
||||||
if (curl_errno($rcurl))
|
|
||||||
{
|
|
||||||
$code = 0;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
$code = curl_getinfo($rcurl, CURLINFO_HTTP_CODE);
|
|
||||||
if ($code == 301 || $code == 302)
|
|
||||||
{
|
|
||||||
$matches = array();
|
|
||||||
preg_match('/Location:(.*?)\n/', $header, $matches);
|
|
||||||
$newurl = trim(array_pop($matches));
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
$code = 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} while ($code && --$mr);
|
|
||||||
|
|
||||||
curl_close($rcurl);
|
|
||||||
if ($mr > 0)
|
|
||||||
{
|
|
||||||
curl_setopt($curl, CURLOPT_URL, $newurl);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($mr == 0 && $max_redirects > 0)
|
|
||||||
{
|
|
||||||
$data = false;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
$data = curl_exec($curl);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return array(
|
|
||||||
$data,
|
|
||||||
curl_getinfo($curl),
|
|
||||||
curl_errno($curl),
|
|
||||||
curl_error($curl)
|
|
||||||
);
|
|
||||||
// END rainloop
|
|
||||||
|
|
||||||
$curl = curl_init($url);
|
|
||||||
curl_setopt_array($curl, $settings);
|
|
||||||
|
|
||||||
return array(
|
|
||||||
curl_exec($curl),
|
|
||||||
curl_getinfo($curl),
|
|
||||||
curl_errno($curl),
|
|
||||||
curl_error($curl)
|
|
||||||
);
|
|
||||||
|
|
||||||
}
|
|
||||||
// @codeCoverageIgnoreEnd
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the full url based on the given url (which may be relative). All
|
|
||||||
* urls are expanded based on the base url as given by the server.
|
|
||||||
*
|
|
||||||
* @param string $url
|
|
||||||
* @return string
|
|
||||||
*/
|
|
||||||
protected function getAbsoluteUrl($url) {
|
|
||||||
|
|
||||||
// If the url starts with http:// or https://, the url is already absolute.
|
|
||||||
if (preg_match('/^http(s?):\/\//', $url)) {
|
|
||||||
return $url;
|
|
||||||
}
|
|
||||||
|
|
||||||
// If the url starts with a slash, we must calculate the url based off
|
|
||||||
// the root of the base url.
|
|
||||||
if (strpos($url,'/') === 0) {
|
|
||||||
$parts = parse_url($this->baseUri);
|
|
||||||
return $parts['scheme'] . '://' . $parts['host'] . (isset($parts['port'])?':' . $parts['port']:'') . $url;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Otherwise...
|
|
||||||
return $this->baseUri . $url;
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Parses a WebDAV multistatus response body
|
|
||||||
*
|
|
||||||
* This method returns an array with the following structure
|
|
||||||
*
|
|
||||||
* array(
|
|
||||||
* 'url/to/resource' => array(
|
|
||||||
* '200' => array(
|
|
||||||
* '{DAV:}property1' => 'value1',
|
|
||||||
* '{DAV:}property2' => 'value2',
|
|
||||||
* ),
|
|
||||||
* '404' => array(
|
|
||||||
* '{DAV:}property1' => null,
|
|
||||||
* '{DAV:}property2' => null,
|
|
||||||
* ),
|
|
||||||
* )
|
|
||||||
* 'url/to/resource2' => array(
|
|
||||||
* .. etc ..
|
|
||||||
* )
|
|
||||||
* )
|
|
||||||
*
|
|
||||||
*
|
|
||||||
* @param string $body xml body
|
|
||||||
* @return array
|
|
||||||
*/
|
|
||||||
public function parseMultiStatus($body) {
|
|
||||||
|
|
||||||
$body = XMLUtil::convertDAVNamespace($body);
|
|
||||||
|
|
||||||
// Fixes an XXE vulnerability on PHP versions older than 5.3.23 or
|
|
||||||
// 5.4.13.
|
|
||||||
$previous = libxml_disable_entity_loader(true);
|
|
||||||
$responseXML = simplexml_load_string($body, null, LIBXML_NOBLANKS | LIBXML_NOCDATA);
|
|
||||||
libxml_disable_entity_loader($previous);
|
|
||||||
|
|
||||||
if ($responseXML===false) {
|
|
||||||
throw new \InvalidArgumentException('The passed data is not valid XML');
|
|
||||||
}
|
|
||||||
|
|
||||||
$responseXML->registerXPathNamespace('d', 'urn:DAV');
|
|
||||||
|
|
||||||
$propResult = array();
|
|
||||||
|
|
||||||
foreach($responseXML->xpath('d:response') as $response) {
|
|
||||||
$response->registerXPathNamespace('d', 'urn:DAV');
|
|
||||||
$href = $response->xpath('d:href');
|
|
||||||
$href = (string)$href[0];
|
|
||||||
|
|
||||||
$properties = array();
|
|
||||||
|
|
||||||
foreach($response->xpath('d:propstat') as $propStat) {
|
|
||||||
|
|
||||||
$propStat->registerXPathNamespace('d', 'urn:DAV');
|
|
||||||
$status = $propStat->xpath('d:status');
|
|
||||||
list($httpVersion, $statusCode, $message) = explode(' ', (string)$status[0],3);
|
|
||||||
|
|
||||||
// Only using the propertymap for results with status 200.
|
|
||||||
$propertyMap = $statusCode==='200' ? $this->propertyMap : array();
|
|
||||||
|
|
||||||
$properties[$statusCode] = XMLUtil::parseProperties(dom_import_simplexml($propStat), $propertyMap);
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
$propResult[$href] = $properties;
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
return $propResult;
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
@ -1,64 +0,0 @@
|
||||||
<?php
|
|
||||||
|
|
||||||
/**
|
|
||||||
* SabreDAV base exception
|
|
||||||
*
|
|
||||||
* This is SabreDAV's base exception file, use this to implement your own exception.
|
|
||||||
*
|
|
||||||
* @copyright Copyright (C) 2007-2015 fruux GmbH (https://fruux.com/).
|
|
||||||
* @author Evert Pot (http://evertpot.com/)
|
|
||||||
* @license http://sabre.io/license/ Modified BSD License
|
|
||||||
*/
|
|
||||||
|
|
||||||
namespace Sabre\DAV;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Main Exception class.
|
|
||||||
*
|
|
||||||
* This class defines a getHTTPCode method, which should return the appropriate HTTP code for the Exception occurred.
|
|
||||||
* The default for this is 500.
|
|
||||||
*
|
|
||||||
* This class also allows you to generate custom xml data for your exceptions. This will be displayed
|
|
||||||
* in the 'error' element in the failing response.
|
|
||||||
*/
|
|
||||||
class Exception extends \Exception {
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the HTTP statuscode for this exception
|
|
||||||
*
|
|
||||||
* @return int
|
|
||||||
*/
|
|
||||||
public function getHTTPCode() {
|
|
||||||
|
|
||||||
return 500;
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* This method allows the exception to include additional information into the WebDAV error response
|
|
||||||
*
|
|
||||||
* @param Server $server
|
|
||||||
* @param \DOMElement $errorNode
|
|
||||||
* @return void
|
|
||||||
*/
|
|
||||||
public function serialize(Server $server,\DOMElement $errorNode) {
|
|
||||||
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* This method allows the exception to return any extra HTTP response headers.
|
|
||||||
*
|
|
||||||
* The headers must be returned as an array.
|
|
||||||
*
|
|
||||||
* @param Server $server
|
|
||||||
* @return array
|
|
||||||
*/
|
|
||||||
public function getHTTPHeaders(Server $server) {
|
|
||||||
|
|
||||||
return array();
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
@ -1,28 +0,0 @@
|
||||||
<?php
|
|
||||||
|
|
||||||
namespace Sabre\DAV\Exception;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* BadRequest
|
|
||||||
*
|
|
||||||
* The BadRequest is thrown when the user submitted an invalid HTTP request
|
|
||||||
* BadRequest
|
|
||||||
*
|
|
||||||
* @copyright Copyright (C) 2007-2015 fruux GmbH (https://fruux.com/).
|
|
||||||
* @author Evert Pot (http://evertpot.com/)
|
|
||||||
* @license http://sabre.io/license/ Modified BSD License
|
|
||||||
*/
|
|
||||||
class BadRequest extends \Sabre\DAV\Exception {
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the HTTP statuscode for this exception
|
|
||||||
*
|
|
||||||
* @return int
|
|
||||||
*/
|
|
||||||
public function getHTTPCode() {
|
|
||||||
|
|
||||||
return 400;
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
@ -1,28 +0,0 @@
|
||||||
<?php
|
|
||||||
|
|
||||||
namespace Sabre\DAV\Exception;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Conflict
|
|
||||||
*
|
|
||||||
* A 409 Conflict is thrown when a user tried to make a directory over an existing
|
|
||||||
* file or in a parent directory that doesn't exist.
|
|
||||||
*
|
|
||||||
* @copyright Copyright (C) 2007-2015 fruux GmbH (https://fruux.com/).
|
|
||||||
* @author Evert Pot (http://evertpot.com/)
|
|
||||||
* @license http://sabre.io/license/ Modified BSD License
|
|
||||||
*/
|
|
||||||
class Conflict extends \Sabre\DAV\Exception {
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the HTTP statuscode for this exception
|
|
||||||
*
|
|
||||||
* @return int
|
|
||||||
*/
|
|
||||||
public function getHTTPCode() {
|
|
||||||
|
|
||||||
return 409;
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
@ -1,27 +0,0 @@
|
||||||
<?php
|
|
||||||
|
|
||||||
namespace Sabre\DAV\Exception;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Forbidden
|
|
||||||
*
|
|
||||||
* This exception is thrown whenever a user tries to do an operation he's not allowed to
|
|
||||||
*
|
|
||||||
* @copyright Copyright (C) 2007-2015 fruux GmbH (https://fruux.com/).
|
|
||||||
* @author Evert Pot (http://evertpot.com/)
|
|
||||||
* @license http://sabre.io/license/ Modified BSD License
|
|
||||||
*/
|
|
||||||
class Forbidden extends \Sabre\DAV\Exception {
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the HTTP statuscode for this exception
|
|
||||||
*
|
|
||||||
* @return int
|
|
||||||
*/
|
|
||||||
public function getHTTPCode() {
|
|
||||||
|
|
||||||
return 403;
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
@ -1,27 +0,0 @@
|
||||||
<?php
|
|
||||||
|
|
||||||
namespace Sabre\DAV\Exception;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* InsufficientStorage
|
|
||||||
*
|
|
||||||
* This Exception can be thrown, when for example a harddisk is full or a quota is exceeded
|
|
||||||
*
|
|
||||||
* @copyright Copyright (C) 2007-2015 fruux GmbH (https://fruux.com/).
|
|
||||||
* @author Evert Pot (http://evertpot.com/)
|
|
||||||
* @license http://sabre.io/license/ Modified BSD License
|
|
||||||
*/
|
|
||||||
class InsufficientStorage extends \Sabre\DAV\Exception {
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the HTTP statuscode for this exception
|
|
||||||
*
|
|
||||||
* @return int
|
|
||||||
*/
|
|
||||||
public function getHTTPCode() {
|
|
||||||
|
|
||||||
return 507;
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
@ -1,45 +0,0 @@
|
||||||
<?php
|
|
||||||
|
|
||||||
namespace Sabre\DAV\Exception;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* MethodNotAllowed
|
|
||||||
*
|
|
||||||
* The 405 is thrown when a client tried to create a directory on an already existing directory
|
|
||||||
*
|
|
||||||
* @copyright Copyright (C) 2007-2015 fruux GmbH (https://fruux.com/).
|
|
||||||
* @author Evert Pot (http://evertpot.com/)
|
|
||||||
* @license http://sabre.io/license/ Modified BSD License
|
|
||||||
*/
|
|
||||||
class MethodNotAllowed extends \Sabre\DAV\Exception {
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the HTTP statuscode for this exception
|
|
||||||
*
|
|
||||||
* @return int
|
|
||||||
*/
|
|
||||||
public function getHTTPCode() {
|
|
||||||
|
|
||||||
return 405;
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* This method allows the exception to return any extra HTTP response headers.
|
|
||||||
*
|
|
||||||
* The headers must be returned as an array.
|
|
||||||
*
|
|
||||||
* @param \Sabre\DAV\Server $server
|
|
||||||
* @return array
|
|
||||||
*/
|
|
||||||
public function getHTTPHeaders(\Sabre\DAV\Server $server) {
|
|
||||||
|
|
||||||
$methods = $server->getAllowedMethods($server->getRequestUri());
|
|
||||||
|
|
||||||
return array(
|
|
||||||
'Allow' => strtoupper(implode(', ',$methods)),
|
|
||||||
);
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
@ -1,30 +0,0 @@
|
||||||
<?php
|
|
||||||
|
|
||||||
namespace Sabre\DAV\Exception;
|
|
||||||
|
|
||||||
use Sabre\DAV;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* NotAuthenticated
|
|
||||||
*
|
|
||||||
* This exception is thrown when the client did not provide valid
|
|
||||||
* authentication credentials.
|
|
||||||
*
|
|
||||||
* @copyright Copyright (C) 2007-2015 fruux GmbH (https://fruux.com/).
|
|
||||||
* @author Evert Pot (http://evertpot.com/)
|
|
||||||
* @license http://sabre.io/license/ Modified BSD License
|
|
||||||
*/
|
|
||||||
class NotAuthenticated extends DAV\Exception {
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the HTTP statuscode for this exception
|
|
||||||
*
|
|
||||||
* @return int
|
|
||||||
*/
|
|
||||||
public function getHTTPCode() {
|
|
||||||
|
|
||||||
return 401;
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
@ -1,28 +0,0 @@
|
||||||
<?php
|
|
||||||
|
|
||||||
namespace Sabre\DAV\Exception;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* NotFound
|
|
||||||
*
|
|
||||||
* This Exception is thrown when a Node couldn't be found. It returns HTTP error code 404
|
|
||||||
*
|
|
||||||
* @copyright Copyright (C) 2007-2015 fruux GmbH (https://fruux.com/).
|
|
||||||
* @author Evert Pot (http://evertpot.com/)
|
|
||||||
* @license http://sabre.io/license/ Modified BSD License
|
|
||||||
*/
|
|
||||||
class NotFound extends \Sabre\DAV\Exception {
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the HTTP statuscode for this exception
|
|
||||||
*
|
|
||||||
* @return int
|
|
||||||
*/
|
|
||||||
public function getHTTPCode() {
|
|
||||||
|
|
||||||
return 404;
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
@ -1,27 +0,0 @@
|
||||||
<?php
|
|
||||||
|
|
||||||
namespace Sabre\DAV\Exception;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* NotImplemented
|
|
||||||
*
|
|
||||||
* This exception is thrown when the client tried to call an unsupported HTTP method or other feature
|
|
||||||
*
|
|
||||||
* @copyright Copyright (C) 2007-2015 fruux GmbH (https://fruux.com/).
|
|
||||||
* @author Evert Pot (http://evertpot.com/)
|
|
||||||
* @license http://sabre.io/license/ Modified BSD License
|
|
||||||
*/
|
|
||||||
class NotImplemented extends \Sabre\DAV\Exception {
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the HTTP statuscode for this exception
|
|
||||||
*
|
|
||||||
* @return int
|
|
||||||
*/
|
|
||||||
public function getHTTPCode() {
|
|
||||||
|
|
||||||
return 501;
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
@ -1,30 +0,0 @@
|
||||||
<?php
|
|
||||||
|
|
||||||
namespace Sabre\DAV\Exception;
|
|
||||||
|
|
||||||
use Sabre\DAV;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Payment Required
|
|
||||||
*
|
|
||||||
* The PaymentRequired exception may be thrown in a case where a user must pay
|
|
||||||
* to access a certain resource or operation.
|
|
||||||
*
|
|
||||||
* @copyright Copyright (C) 2007-2015 fruux GmbH (https://fruux.com/).
|
|
||||||
* @author Evert Pot (http://evertpot.com/)
|
|
||||||
* @license http://sabre.io/license/ Modified BSD License
|
|
||||||
*/
|
|
||||||
class PaymentRequired extends DAV\Exception {
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the HTTP statuscode for this exception
|
|
||||||
*
|
|
||||||
* @return int
|
|
||||||
*/
|
|
||||||
public function getHTTPCode() {
|
|
||||||
|
|
||||||
return 402;
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
@ -1,71 +0,0 @@
|
||||||
<?php
|
|
||||||
|
|
||||||
namespace Sabre\DAV\Exception;
|
|
||||||
|
|
||||||
use Sabre\DAV;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* PreconditionFailed
|
|
||||||
*
|
|
||||||
* This exception is normally thrown when a client submitted a conditional request,
|
|
||||||
* like for example an If, If-None-Match or If-Match header, which caused the HTTP
|
|
||||||
* request to not execute (the condition of the header failed)
|
|
||||||
*
|
|
||||||
* @copyright Copyright (C) 2007-2015 fruux GmbH (https://fruux.com/).
|
|
||||||
* @author Evert Pot (http://evertpot.com/)
|
|
||||||
* @license http://sabre.io/license/ Modified BSD License
|
|
||||||
*/
|
|
||||||
class PreconditionFailed extends DAV\Exception {
|
|
||||||
|
|
||||||
/**
|
|
||||||
* When this exception is thrown, the header-name might be set.
|
|
||||||
*
|
|
||||||
* This allows the exception-catching code to determine which HTTP header
|
|
||||||
* caused the exception.
|
|
||||||
*
|
|
||||||
* @var string
|
|
||||||
*/
|
|
||||||
public $header = null;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Create the exception
|
|
||||||
*
|
|
||||||
* @param string $message
|
|
||||||
* @param string $header
|
|
||||||
*/
|
|
||||||
public function __construct($message, $header=null) {
|
|
||||||
|
|
||||||
parent::__construct($message);
|
|
||||||
$this->header = $header;
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the HTTP statuscode for this exception
|
|
||||||
*
|
|
||||||
* @return int
|
|
||||||
*/
|
|
||||||
public function getHTTPCode() {
|
|
||||||
|
|
||||||
return 412;
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* This method allows the exception to include additional information into the WebDAV error response
|
|
||||||
*
|
|
||||||
* @param DAV\Server $server
|
|
||||||
* @param \DOMElement $errorNode
|
|
||||||
* @return void
|
|
||||||
*/
|
|
||||||
public function serialize(DAV\Server $server,\DOMElement $errorNode) {
|
|
||||||
|
|
||||||
if ($this->header) {
|
|
||||||
$prop = $errorNode->ownerDocument->createElement('s:header');
|
|
||||||
$prop->nodeValue = $this->header;
|
|
||||||
$errorNode->appendChild($prop);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
@ -1,31 +0,0 @@
|
||||||
<?php
|
|
||||||
|
|
||||||
namespace Sabre\DAV\Exception;
|
|
||||||
|
|
||||||
use Sabre\DAV;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* RequestedRangeNotSatisfiable
|
|
||||||
*
|
|
||||||
* This exception is normally thrown when the user
|
|
||||||
* request a range that is out of the entity bounds.
|
|
||||||
*
|
|
||||||
* @copyright Copyright (C) 2007-2015 fruux GmbH (https://fruux.com/).
|
|
||||||
* @author Evert Pot (http://evertpot.com/)
|
|
||||||
* @license http://sabre.io/license/ Modified BSD License
|
|
||||||
*/
|
|
||||||
class RequestedRangeNotSatisfiable extends DAV\Exception {
|
|
||||||
|
|
||||||
/**
|
|
||||||
* returns the http statuscode for this exception
|
|
||||||
*
|
|
||||||
* @return int
|
|
||||||
*/
|
|
||||||
public function getHTTPCode() {
|
|
||||||
|
|
||||||
return 416;
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
@ -1,191 +0,0 @@
|
||||||
<?php
|
|
||||||
|
|
||||||
namespace Sabre\DAV;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* XML utilities for WebDAV
|
|
||||||
*
|
|
||||||
* @copyright Copyright (C) 2007-2015 fruux GmbH (https://fruux.com/).
|
|
||||||
* @author Evert Pot (http://evertpot.com/)
|
|
||||||
* @license http://sabre.io/license/ Modified BSD License
|
|
||||||
*/
|
|
||||||
class XMLUtil {
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the 'clark notation' for an element.
|
|
||||||
*
|
|
||||||
* For example, and element encoded as:
|
|
||||||
* <b:myelem xmlns:b="http://www.example.org/" />
|
|
||||||
* will be returned as:
|
|
||||||
* {http://www.example.org}myelem
|
|
||||||
*
|
|
||||||
* This format is used throughout the SabreDAV sourcecode.
|
|
||||||
* Elements encoded with the urn:DAV namespace will
|
|
||||||
* be returned as if they were in the DAV: namespace. This is to avoid
|
|
||||||
* compatibility problems.
|
|
||||||
*
|
|
||||||
* This function will return null if a nodetype other than an Element is passed.
|
|
||||||
*
|
|
||||||
* @param \DOMNode $dom
|
|
||||||
* @return string
|
|
||||||
*/
|
|
||||||
static function toClarkNotation(\DOMNode $dom) {
|
|
||||||
|
|
||||||
if ($dom->nodeType !== XML_ELEMENT_NODE) return null;
|
|
||||||
|
|
||||||
// Mapping back to the real namespace, in case it was dav
|
|
||||||
if ($dom->namespaceURI=='urn:DAV') $ns = 'DAV:'; else $ns = $dom->namespaceURI;
|
|
||||||
|
|
||||||
// Mapping to clark notation
|
|
||||||
return '{' . $ns . '}' . $dom->localName;
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Parses a clark-notation string, and returns the namespace and element
|
|
||||||
* name components.
|
|
||||||
*
|
|
||||||
* If the string was invalid, it will throw an InvalidArgumentException.
|
|
||||||
*
|
|
||||||
* @param string $str
|
|
||||||
* @throws InvalidArgumentException
|
|
||||||
* @return array
|
|
||||||
*/
|
|
||||||
static function parseClarkNotation($str) {
|
|
||||||
|
|
||||||
if (!preg_match('/^{([^}]*)}(.*)$/',$str,$matches)) {
|
|
||||||
throw new \InvalidArgumentException('\'' . $str . '\' is not a valid clark-notation formatted string');
|
|
||||||
}
|
|
||||||
|
|
||||||
return array(
|
|
||||||
$matches[1],
|
|
||||||
$matches[2]
|
|
||||||
);
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* This method takes an XML document (as string) and converts all instances of the
|
|
||||||
* DAV: namespace to urn:DAV
|
|
||||||
*
|
|
||||||
* This is unfortunately needed, because the DAV: namespace violates the xml namespaces
|
|
||||||
* spec, and causes the DOM to throw errors
|
|
||||||
*
|
|
||||||
* @param string $xmlDocument
|
|
||||||
* @return array|string|null
|
|
||||||
*/
|
|
||||||
static function convertDAVNamespace($xmlDocument) {
|
|
||||||
|
|
||||||
// This is used to map the DAV: namespace to urn:DAV. This is needed, because the DAV:
|
|
||||||
// namespace is actually a violation of the XML namespaces specification, and will cause errors
|
|
||||||
return preg_replace("/xmlns(:[A-Za-z0-9_]*)?=(\"|\')DAV:(\\2)/","xmlns\\1=\\2urn:DAV\\2",$xmlDocument);
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* This method provides a generic way to load a DOMDocument for WebDAV use.
|
|
||||||
*
|
|
||||||
* This method throws a Sabre\DAV\Exception\BadRequest exception for any xml errors.
|
|
||||||
* It does not preserve whitespace, and it converts the DAV: namespace to urn:DAV.
|
|
||||||
*
|
|
||||||
* @param string $xml
|
|
||||||
* @throws Sabre\DAV\Exception\BadRequest
|
|
||||||
* @return DOMDocument
|
|
||||||
*/
|
|
||||||
static function loadDOMDocument($xml) {
|
|
||||||
|
|
||||||
if (empty($xml))
|
|
||||||
throw new Exception\BadRequest('Empty XML document sent');
|
|
||||||
|
|
||||||
// The BitKinex client sends xml documents as UTF-16. PHP 5.3.1 (and presumably lower)
|
|
||||||
// does not support this, so we must intercept this and convert to UTF-8.
|
|
||||||
if (substr($xml,0,12) === "\x3c\x00\x3f\x00\x78\x00\x6d\x00\x6c\x00\x20\x00") {
|
|
||||||
|
|
||||||
// Note: the preceeding byte sequence is "<?xml" encoded as UTF_16, without the BOM.
|
|
||||||
$xml = iconv('UTF-16LE','UTF-8',$xml);
|
|
||||||
|
|
||||||
// Because the xml header might specify the encoding, we must also change this.
|
|
||||||
// This regex looks for the string encoding="UTF-16" and replaces it with
|
|
||||||
// encoding="UTF-8".
|
|
||||||
$xml = preg_replace('|<\?xml([^>]*)encoding="UTF-16"([^>]*)>|u','<?xml\1encoding="UTF-8"\2>',$xml);
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
// Retaining old error setting
|
|
||||||
$oldErrorSetting = libxml_use_internal_errors(true);
|
|
||||||
// Fixes an XXE vulnerability on PHP versions older than 5.3.23 or
|
|
||||||
// 5.4.13.
|
|
||||||
$oldEntityLoaderSetting = libxml_disable_entity_loader(true);
|
|
||||||
|
|
||||||
// Clearing any previous errors
|
|
||||||
libxml_clear_errors();
|
|
||||||
|
|
||||||
$dom = new \DOMDocument();
|
|
||||||
|
|
||||||
// We don't generally care about any whitespace
|
|
||||||
$dom->preserveWhiteSpace = false;
|
|
||||||
|
|
||||||
$dom->loadXML(self::convertDAVNamespace($xml),LIBXML_NOWARNING | LIBXML_NOERROR);
|
|
||||||
|
|
||||||
if ($error = libxml_get_last_error()) {
|
|
||||||
libxml_clear_errors();
|
|
||||||
throw new Exception\BadRequest('The request body had an invalid XML body. (message: ' . $error->message . ', errorcode: ' . $error->code . ', line: ' . $error->line . ')');
|
|
||||||
}
|
|
||||||
|
|
||||||
// Restoring old mechanism for error handling
|
|
||||||
if ($oldErrorSetting===false) libxml_use_internal_errors(false);
|
|
||||||
if ($oldEntityLoaderSetting===false) libxml_disable_entity_loader(false);
|
|
||||||
|
|
||||||
return $dom;
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Parses all WebDAV properties out of a DOM Element
|
|
||||||
*
|
|
||||||
* Generally WebDAV properties are enclosed in {DAV:}prop elements. This
|
|
||||||
* method helps by going through all these and pulling out the actual
|
|
||||||
* propertynames, making them array keys and making the property values,
|
|
||||||
* well.. the array values.
|
|
||||||
*
|
|
||||||
* If no value was given (self-closing element) null will be used as the
|
|
||||||
* value. This is used in for example PROPFIND requests.
|
|
||||||
*
|
|
||||||
* Complex values are supported through the propertyMap argument. The
|
|
||||||
* propertyMap should have the clark-notation properties as it's keys, and
|
|
||||||
* classnames as values.
|
|
||||||
*
|
|
||||||
* When any of these properties are found, the unserialize() method will be
|
|
||||||
* (statically) called. The result of this method is used as the value.
|
|
||||||
*
|
|
||||||
* @param \DOMElement $parentNode
|
|
||||||
* @param array $propertyMap
|
|
||||||
* @return array
|
|
||||||
*/
|
|
||||||
static function parseProperties(\DOMElement $parentNode, array $propertyMap = array()) {
|
|
||||||
|
|
||||||
$propList = array();
|
|
||||||
foreach($parentNode->childNodes as $propNode) {
|
|
||||||
|
|
||||||
if (self::toClarkNotation($propNode)!=='{DAV:}prop') continue;
|
|
||||||
|
|
||||||
foreach($propNode->childNodes as $propNodeData) {
|
|
||||||
|
|
||||||
/* If there are no elements in here, we actually get 1 text node, this special case is dedicated to netdrive */
|
|
||||||
if ($propNodeData->nodeType != XML_ELEMENT_NODE) continue;
|
|
||||||
|
|
||||||
$propertyName = self::toClarkNotation($propNodeData);
|
|
||||||
if (isset($propertyMap[$propertyName])) {
|
|
||||||
$propList[$propertyName] = call_user_func(array($propertyMap[$propertyName],'unserialize'),$propNodeData);
|
|
||||||
} else {
|
|
||||||
$propList[$propertyName] = $propNodeData->textContent;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
}
|
|
||||||
return $propList;
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
237
snappymail/v/0.0.0/app/libraries/snappymail/dav/client.php
Normal file
237
snappymail/v/0.0.0/app/libraries/snappymail/dav/client.php
Normal file
|
|
@ -0,0 +1,237 @@
|
||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* Based on Sabre\DAV\Client
|
||||||
|
* @copyright Copyright (C) 2007-2015 fruux GmbH (https://fruux.com/).
|
||||||
|
* @author Evert Pot (http://evertpot.com/)
|
||||||
|
* @license http://sabre.io/license/ Modified BSD License
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace SnappyMail\DAV;
|
||||||
|
|
||||||
|
class Client
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* The propertyMap is a key-value array.
|
||||||
|
*
|
||||||
|
* If you use the propertyMap, any {DAV:}multistatus responses with the
|
||||||
|
* proeprties listed in this array, will automatically be mapped to a
|
||||||
|
* respective class.
|
||||||
|
*
|
||||||
|
* The {DAV:}resourcetype property is automatically added. This maps to
|
||||||
|
* Sabre\DAV\Property\ResourceType
|
||||||
|
*
|
||||||
|
* @var array
|
||||||
|
*/
|
||||||
|
public $propertyMap = array(
|
||||||
|
// '{DAV:}resourcetype' => 'SnappyMail\\DAV\\Property\\ResourceType'
|
||||||
|
);
|
||||||
|
|
||||||
|
protected $baseUri;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constructor
|
||||||
|
*
|
||||||
|
* Settings are provided through the 'settings' argument. The following
|
||||||
|
* settings are supported:
|
||||||
|
*
|
||||||
|
* * baseUri
|
||||||
|
* * userName (optional)
|
||||||
|
* * password (optional)
|
||||||
|
* * proxy (optional)
|
||||||
|
*/
|
||||||
|
function __construct(array $settings)
|
||||||
|
{
|
||||||
|
if (!isset($settings['baseUri'])) {
|
||||||
|
throw new \InvalidArgumentException('A baseUri must be provided');
|
||||||
|
}
|
||||||
|
$this->baseUri = $settings['baseUri'];
|
||||||
|
|
||||||
|
$this->HTTP = \SnappyMail\HTTP\Request::factory('socket');
|
||||||
|
$this->HTTP->proxy = $settings['proxy'] ?? null;
|
||||||
|
$this->HTTP->setAuth(3, $settings['userName'] ?? '', $settings['password'] ?? '');
|
||||||
|
$this->HTTP->max_response_kb = 0;
|
||||||
|
$this->HTTP->timeout = 15; // timeout in seconds.
|
||||||
|
$this->HTTP->follow_location = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Enable/disable SSL peer verification
|
||||||
|
*/
|
||||||
|
public function setVerifyPeer(bool $value) : void
|
||||||
|
{
|
||||||
|
$this->HTTP->verify_peer = $value;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Does a PROPFIND request
|
||||||
|
*
|
||||||
|
* The list of requested properties must be specified as an array, in clark
|
||||||
|
* notation.
|
||||||
|
*
|
||||||
|
* The returned array will contain a list of filenames as keys, and
|
||||||
|
* properties as values.
|
||||||
|
*
|
||||||
|
* The properties array will contain the list of properties. Only properties
|
||||||
|
* that are actually returned from the server (without error) will be
|
||||||
|
* returned, anything else is discarded.
|
||||||
|
*
|
||||||
|
* Depth should be either 0 or 1. A depth of 1 will cause a request to be
|
||||||
|
* made to the server to also return all child resources.
|
||||||
|
*/
|
||||||
|
public function propFind(string $url, array $properties, int $depth = 0) : array
|
||||||
|
{
|
||||||
|
$body = '<?xml version="1.0"?>' . "\n";
|
||||||
|
$body.= '<d:propfind xmlns:d="DAV:">' . "\n";
|
||||||
|
$body.= ' <d:prop>' . "\n";
|
||||||
|
|
||||||
|
foreach ($properties as $property) {
|
||||||
|
if (!\preg_match('/^{([^}]*)}(.*)$/', $property, $match)) {
|
||||||
|
throw new \InvalidArgumentException('\'' . $property . '\' is not a valid clark-notation formatted string');
|
||||||
|
}
|
||||||
|
if ('DAV:' === $match[1]) {
|
||||||
|
$body .= " <d:{$match[2]} />\n";
|
||||||
|
} else {
|
||||||
|
$body .= " <x:{$match[2]} xmlns:x=\"{$match[1]}\"/>\n";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$body .= ' </d:prop>' . "\n";
|
||||||
|
$body .= '</d:propfind>';
|
||||||
|
|
||||||
|
if (!\preg_match('/^http(s?):\/\//', $url)) {
|
||||||
|
// If the url starts with a slash, we must calculate the url based off
|
||||||
|
// the root of the base url.
|
||||||
|
if (0 === \strpos($url, '/')) {
|
||||||
|
$parts = \parse_url($this->baseUri);
|
||||||
|
$url = $parts['scheme'] . '://' . $parts['host'] . (isset($parts['port'])?':' . $parts['port']:'') . $url;
|
||||||
|
} else {
|
||||||
|
$url = $this->baseUri . $url;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$response = $this->HTTP->doRequest('PROPFIND', $url, $body, array(
|
||||||
|
"Depth: {$depth}",
|
||||||
|
'Content-Type: application/xml'
|
||||||
|
));
|
||||||
|
if (300 <= $response->status) {
|
||||||
|
throw new \SnappyMail\HTTP\Exception('', $response->status);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parse the WebDAV multistatus response body
|
||||||
|
*/
|
||||||
|
$responseXML = \simplexml_load_string(
|
||||||
|
/**
|
||||||
|
* Convert all instances of the DAV: namespace to urn:DAV
|
||||||
|
*
|
||||||
|
* This is unfortunately needed, because the DAV: namespace violates the xml namespaces
|
||||||
|
* spec, and causes the DOM to throw errors
|
||||||
|
*
|
||||||
|
* This is used to map the DAV: namespace to urn:DAV. This is needed, because the DAV:
|
||||||
|
* namespace is actually a violation of the XML namespaces specification, and will cause errors
|
||||||
|
*/
|
||||||
|
\preg_replace("/xmlns(:[A-Za-z0-9_]*)?=(\"|\')DAV:(\\2)/", "xmlns\\1=\\2urn:DAV\\2", $response->body),
|
||||||
|
null, LIBXML_NOBLANKS | LIBXML_NOCDATA);
|
||||||
|
|
||||||
|
if (false === $responseXML) {
|
||||||
|
throw new \InvalidArgumentException('The passed data is not valid XML');
|
||||||
|
}
|
||||||
|
|
||||||
|
$responseXML->registerXPathNamespace('d', 'urn:DAV');
|
||||||
|
|
||||||
|
$result = array();
|
||||||
|
|
||||||
|
foreach ($responseXML->xpath('d:response') as $response) {
|
||||||
|
$response->registerXPathNamespace('d', 'urn:DAV');
|
||||||
|
$href = $response->xpath('d:href');
|
||||||
|
$href = (string) $href[0];
|
||||||
|
|
||||||
|
$properties = array();
|
||||||
|
|
||||||
|
foreach ($response->xpath('d:propstat') as $propStat) {
|
||||||
|
$propStat->registerXPathNamespace('d', 'urn:DAV');
|
||||||
|
$status = $propStat->xpath('d:status');
|
||||||
|
list($httpVersion, $statusCode, $message) = \explode(' ', (string)$status[0], 3);
|
||||||
|
|
||||||
|
// Only using the propertymap for results with status 200.
|
||||||
|
$propertyMap = $statusCode === '200' ? $this->propertyMap : array();
|
||||||
|
|
||||||
|
$properties[$statusCode] = static::parseProperties(\dom_import_simplexml($propStat), $propertyMap);
|
||||||
|
}
|
||||||
|
|
||||||
|
$result[$href] = $properties;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (0 === $depth) {
|
||||||
|
\reset($result);
|
||||||
|
return \current($result)[200] ?? array();
|
||||||
|
}
|
||||||
|
|
||||||
|
return \array_map(function($statusList){
|
||||||
|
return $statusList[200] ?? array();
|
||||||
|
}, $result);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the 'clark notation' for an element.
|
||||||
|
*
|
||||||
|
* For example, and element encoded as:
|
||||||
|
* <b:myelem xmlns:b="http://www.example.org/" />
|
||||||
|
* will be returned as:
|
||||||
|
* {http://www.example.org}myelem
|
||||||
|
*
|
||||||
|
* This format is used throughout the SabreDAV sourcecode.
|
||||||
|
* Elements encoded with the urn:DAV namespace will
|
||||||
|
* be returned as if they were in the DAV: namespace. This is to avoid
|
||||||
|
* compatibility problems.
|
||||||
|
*
|
||||||
|
* This function will return null if a nodetype other than an Element is passed.
|
||||||
|
*/
|
||||||
|
public static function toClarkNotation(\DOMNode $dom) : ?string
|
||||||
|
{
|
||||||
|
// Mapping back to the real namespace, in case it was dav
|
||||||
|
// Mapping to clark notation
|
||||||
|
return XML_ELEMENT_NODE === $dom->nodeType
|
||||||
|
? '{' . ('urn:DAV' == $dom->namespaceURI ? 'DAV:' : $dom->namespaceURI) . '}' . $dom->localName
|
||||||
|
: null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parses all WebDAV properties out of a DOM Element
|
||||||
|
*
|
||||||
|
* Generally WebDAV properties are enclosed in {DAV:}prop elements. This
|
||||||
|
* method helps by going through all these and pulling out the actual
|
||||||
|
* propertynames, making them array keys and making the property values,
|
||||||
|
* well.. the array values.
|
||||||
|
*
|
||||||
|
* If no value was given (self-closing element) null will be used as the
|
||||||
|
* value. This is used in for example PROPFIND requests.
|
||||||
|
*
|
||||||
|
* Complex values are supported through the propertyMap argument. The
|
||||||
|
* propertyMap should have the clark-notation properties as it's keys, and
|
||||||
|
* classnames as values.
|
||||||
|
*
|
||||||
|
* When any of these properties are found, the fromDOMElement() method will be
|
||||||
|
* (statically) called. The result of this method is used as the value.
|
||||||
|
*/
|
||||||
|
protected static function parseProperties(\DOMElement $parentNode, array $propertyMap = array()) : array
|
||||||
|
{
|
||||||
|
$propList = array();
|
||||||
|
foreach ($parentNode->childNodes as $propNode) {
|
||||||
|
if ('{DAV:}prop' === self::toClarkNotation($propNode)) {
|
||||||
|
foreach ($propNode->childNodes as $propNodeData) {
|
||||||
|
/* If there are no elements in here, we actually get 1 text node, this special case is dedicated to netdrive */
|
||||||
|
if (XML_ELEMENT_NODE == $propNodeData->nodeType) {
|
||||||
|
$propertyName = self::toClarkNotation($propNodeData);
|
||||||
|
if (isset($propertyMap[$propertyName])) {
|
||||||
|
$propList[$propertyName] = \call_user_func(array($propertyMap[$propertyName], 'fromDOMElement'), $propNodeData);
|
||||||
|
} else {
|
||||||
|
$propList[$propertyName] = $propNodeData->textContent;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return $propList;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,84 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace SnappyMail\HTTP;
|
||||||
|
|
||||||
|
class Exception extends \Exception
|
||||||
|
{
|
||||||
|
|
||||||
|
# Status Codes https://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html
|
||||||
|
const CODES = array(
|
||||||
|
|
||||||
|
// Redirection 3xx
|
||||||
|
300 => 'Multiple Choices',
|
||||||
|
301 => 'Moved Permanently',
|
||||||
|
302 => 'Found',
|
||||||
|
303 => 'See Other',
|
||||||
|
304 => 'Not Modified',
|
||||||
|
305 => 'Use Proxy',
|
||||||
|
// 306 => 'Switch Proxy', # obsolete
|
||||||
|
307 => 'Temporary Redirect',
|
||||||
|
|
||||||
|
// Client Error 4xx
|
||||||
|
400 => 'Bad Request',
|
||||||
|
401 => 'Unauthorized',
|
||||||
|
402 => 'Payment Required', # reserved for future use
|
||||||
|
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',
|
||||||
|
// https://tools.ietf.org/html/rfc7540#section-9.1.2
|
||||||
|
421 => 'Misdirected Request',
|
||||||
|
// https://tools.ietf.org/html/rfc4918
|
||||||
|
422 => 'Unprocessable Entity',
|
||||||
|
423 => 'Locked',
|
||||||
|
424 => 'Failed Dependency',
|
||||||
|
// http://tools.ietf.org/html/rfc2817
|
||||||
|
426 => 'Upgrade Required',
|
||||||
|
// http://tools.ietf.org/html/rfc6585
|
||||||
|
428 => 'Precondition Required',
|
||||||
|
429 => 'Too Many Requests',
|
||||||
|
431 => 'Request Header Fields Too Large',
|
||||||
|
451 => 'Unavailable For Legal Reasons',
|
||||||
|
|
||||||
|
// Server Error 5xx
|
||||||
|
500 => 'Internal Server Error',
|
||||||
|
501 => 'Not Implemented',
|
||||||
|
502 => 'Bad Gateway',
|
||||||
|
503 => 'Service Unavailable', # may have Retry-After header
|
||||||
|
504 => 'Gateway Timeout',
|
||||||
|
505 => 'HTTP Version Not Supported',
|
||||||
|
// 506 => 'Variant Also Negotiates',
|
||||||
|
// 507 => 'Insufficient Storage',
|
||||||
|
// 508 => 'Loop Detected',
|
||||||
|
// 509 => 'Bandwidth Limit Exceeded',
|
||||||
|
// 510 => 'Not Extended',
|
||||||
|
511 => 'Network Authentication Required',
|
||||||
|
);
|
||||||
|
|
||||||
|
function __construct(string $message = "", int $code = 0, Response $response = null)
|
||||||
|
{
|
||||||
|
if ($response) {
|
||||||
|
if (\in_array($code, array(301, 302, 303, 307))) {
|
||||||
|
$message = $response->getRedirectLocation() . "\n" . $message;
|
||||||
|
} else if (405 === $code && ($allow = $this->getHeader('allow'))) {
|
||||||
|
$message = (\is_array($allow) ? $allow[0] : $allow) . "\n" . $message;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (isset(static::CODES[$code])) {
|
||||||
|
$message = "{$code} " . static::CODES[$code] . ($message ? ": {$message}" : '');
|
||||||
|
}
|
||||||
|
parent::__construct($message, $code);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
159
snappymail/v/0.0.0/app/libraries/snappymail/http/request.php
Normal file
159
snappymail/v/0.0.0/app/libraries/snappymail/http/request.php
Normal file
|
|
@ -0,0 +1,159 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace SnappyMail\HTTP;
|
||||||
|
|
||||||
|
abstract class Request
|
||||||
|
{
|
||||||
|
const
|
||||||
|
/**
|
||||||
|
* Authentication
|
||||||
|
*/
|
||||||
|
AUTH_BASIC = 1,
|
||||||
|
AUTH_DIGEST = 2;
|
||||||
|
|
||||||
|
public
|
||||||
|
$timeout = 5, // timeout in seconds.
|
||||||
|
$max_response_kb = 1024,
|
||||||
|
$user_agent,
|
||||||
|
$follow_location = true,
|
||||||
|
$verify_peer = false,
|
||||||
|
$proxy = null;
|
||||||
|
|
||||||
|
protected
|
||||||
|
$auth = [
|
||||||
|
'type' => 0,
|
||||||
|
'user' => '',
|
||||||
|
'pass' => ''
|
||||||
|
],
|
||||||
|
$stream = null,
|
||||||
|
$headers = array(),
|
||||||
|
$ca_bundle = null;
|
||||||
|
|
||||||
|
protected static $scheme_ports = array(
|
||||||
|
'http' => 80,
|
||||||
|
'https' => 443
|
||||||
|
);
|
||||||
|
|
||||||
|
public static function factory(string $type = 'curl')
|
||||||
|
{
|
||||||
|
if ('curl' === $type && \function_exists('curl_init')) {
|
||||||
|
return new Request\CURL();
|
||||||
|
}
|
||||||
|
return new Request\Socket();
|
||||||
|
}
|
||||||
|
|
||||||
|
function __construct()
|
||||||
|
{
|
||||||
|
$this->user_agent = 'SnappyMail/' . APP_VERSION;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setAuth(int $type, string $user, string $pass) : void
|
||||||
|
{
|
||||||
|
$this->auth = [
|
||||||
|
'type' => $type,
|
||||||
|
'user' => $user,
|
||||||
|
'pass' => $pass
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function addHeader($header)
|
||||||
|
{
|
||||||
|
$this->headers[] = $header;
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function streamBodyTo($stream)
|
||||||
|
{
|
||||||
|
if (!\is_resource($stream)) {
|
||||||
|
throw new \Exception('Invalid body target');
|
||||||
|
}
|
||||||
|
$this->stream = $stream;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setCABundleFile($file)
|
||||||
|
{
|
||||||
|
$this->ca_bundle = $file;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return whether a URI can be fetched. Returns false if the URI scheme is not allowed
|
||||||
|
* or is not supported by this fetcher implementation; returns true otherwise.
|
||||||
|
*
|
||||||
|
* @return bool
|
||||||
|
*/
|
||||||
|
public function canFetchURI($uri)
|
||||||
|
{
|
||||||
|
if ('https:' === \substr($uri, 0, 6) && !$this->supportsSSL()) {
|
||||||
|
\trigger_error('HTTPS URI unsupported fetching '.$uri, E_USER_WARNING);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (!self::URIHasAllowedScheme($uri)) {
|
||||||
|
\trigger_error('URI fetching not allowed for '.$uri, E_USER_WARNING);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Does this fetcher implementation (and runtime) support fetching HTTPS URIs?
|
||||||
|
* May inspect the runtime environment.
|
||||||
|
*
|
||||||
|
* @return bool $support True if this fetcher supports HTTPS
|
||||||
|
* fetching; false if not.
|
||||||
|
*/
|
||||||
|
abstract public function supportsSSL() : bool;
|
||||||
|
|
||||||
|
abstract protected function __doRequest(string &$method, string &$request_url, &$body, array $extra_headers) : Response;
|
||||||
|
|
||||||
|
public function doRequest($method, $request_url, $body = null, array $extra_headers = array()) : ?Response
|
||||||
|
{
|
||||||
|
$method = \strtoupper($method);
|
||||||
|
$url = $request_url;
|
||||||
|
$etime = \time() + $this->timeout;
|
||||||
|
if (\is_array($body)) { $body = \http_build_query($body, '', '&'); }
|
||||||
|
if ($body && 'GET' === $method) {
|
||||||
|
$url .= (\strpos($url, '?')?'&':'?').$body;
|
||||||
|
$body = null;
|
||||||
|
}
|
||||||
|
do
|
||||||
|
{
|
||||||
|
if (!$this->canFetchURI($url)) {
|
||||||
|
throw new \RuntimeException("Can't fetch URL: {$url}");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!self::URIHasAllowedScheme($url)) {
|
||||||
|
throw new \RuntimeException("Fetching URL not allowed: {$url}");
|
||||||
|
}
|
||||||
|
|
||||||
|
$result = $this->__doRequest($method, $url, $body, \array_merge($this->headers, $extra_headers));
|
||||||
|
|
||||||
|
// http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html#sec10.3
|
||||||
|
// In response to a request other than GET or HEAD, the user agent MUST NOT
|
||||||
|
// automatically redirect the request unless it can be confirmed by the user
|
||||||
|
if ($this->follow_location && \is_null($body) && \in_array($result->status, array(301, 302, 303, 307))) {
|
||||||
|
$url = $result->getRedirectLocation();
|
||||||
|
} else {
|
||||||
|
$result->final_uri = $url;
|
||||||
|
$result->request_uri = $request_url;
|
||||||
|
return $result;
|
||||||
|
}
|
||||||
|
|
||||||
|
} while ($etime-time() > 0);
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return whether a URI should be allowed. Override this method to conform to your local policy.
|
||||||
|
* By default, will attempt to fetch any http or https URI.
|
||||||
|
*/
|
||||||
|
public static function URIHasAllowedScheme($uri) : bool
|
||||||
|
{
|
||||||
|
return (bool) \preg_match('#^https?://#i', $uri);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function getSchemePort($scheme) : int
|
||||||
|
{
|
||||||
|
return self::$scheme_ports[$scheme] ?? 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,115 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace SnappyMail\HTTP\Request;
|
||||||
|
|
||||||
|
use \SnappyMail\HTTP\Response;
|
||||||
|
|
||||||
|
class CURL extends \SnappyMail\HTTP\Request
|
||||||
|
{
|
||||||
|
private
|
||||||
|
$response_headers = array(),
|
||||||
|
$response_body = '';
|
||||||
|
|
||||||
|
public function supportsSSL() : bool
|
||||||
|
{
|
||||||
|
$v = \curl_version();
|
||||||
|
if (\is_array($v)) {
|
||||||
|
return \in_array('https', $v['protocols']);
|
||||||
|
}
|
||||||
|
return \is_string($v) ? !!\preg_match('/OpenSSL/i', $v) : false;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function __doRequest(string &$method, string &$request_url, &$body, array $extra_headers) : Response
|
||||||
|
{
|
||||||
|
$c = \curl_init();
|
||||||
|
if (false === $c) {
|
||||||
|
throw new \RuntimeException("Could not initialize CURL for URL '{$request_url}'");
|
||||||
|
}
|
||||||
|
|
||||||
|
$cv = \curl_version();
|
||||||
|
// php.net/curl_setopt
|
||||||
|
\curl_setopt_array($c, array(
|
||||||
|
CURLOPT_USERAGENT => $this->user_agent,
|
||||||
|
CURLOPT_CONNECTTIMEOUT => $this->timeout,
|
||||||
|
CURLOPT_TIMEOUT => $this->timeout,
|
||||||
|
CURLOPT_URL => $request_url,
|
||||||
|
CURLOPT_HEADERFUNCTION => array($this, 'fetchHeader'),
|
||||||
|
CURLOPT_WRITEFUNCTION => array($this, \is_resource($this->stream) ? 'streamData' : 'fetchData'),
|
||||||
|
CURLOPT_SSL_VERIFYPEER => ($this->verify_peer || $this->ca_bundle),
|
||||||
|
// CURLOPT_SSL_VERIFYHOST => $this->verify_peer ? 2 : 0,
|
||||||
|
// CURLOPT_FOLLOWLOCATION => false, // follow redirects
|
||||||
|
// CURLOPT_MAXREDIRS => 0, // stop after 0 redirects
|
||||||
|
));
|
||||||
|
// \curl_setopt($c, CURLOPT_ENCODING , 'gzip');
|
||||||
|
if (\defined('CURLOPT_NOSIGNAL')) {
|
||||||
|
\curl_setopt($c, CURLOPT_NOSIGNAL, true);
|
||||||
|
}
|
||||||
|
if ($this->ca_bundle) {
|
||||||
|
\curl_setopt($c, CURLOPT_CAINFO, $this->ca_bundle);
|
||||||
|
}
|
||||||
|
if ($extra_headers) {
|
||||||
|
\curl_setopt($c, CURLOPT_HTTPHEADER, $extra_headers);
|
||||||
|
}
|
||||||
|
if ($this->auth['user'] && $this->auth['type']) {
|
||||||
|
$auth = 0;
|
||||||
|
if ($this->auth['type'] & self::AUTH_BASIC) {
|
||||||
|
$auth |= CURLAUTH_BASIC;
|
||||||
|
}
|
||||||
|
if ($this->auth['type'] & self::AUTH_DIGEST) {
|
||||||
|
$auth |= CURLAUTH_DIGEST;
|
||||||
|
}
|
||||||
|
\curl_setopt($c, CURLOPT_HTTPAUTH, $auth);
|
||||||
|
\curl_setopt($c, CURLOPT_USERPWD, $this->auth['user'] . ':' . $this->auth['pass']);
|
||||||
|
}
|
||||||
|
if ($this->proxy) {
|
||||||
|
\curl_setopt($c, CURLOPT_PROXY, $this->proxy);
|
||||||
|
}
|
||||||
|
if ('HEAD' === $method) {
|
||||||
|
\curl_setopt($c, CURLOPT_NOBODY, true);
|
||||||
|
} else if ('GET' !== $method) {
|
||||||
|
if ('POST' === $method) {
|
||||||
|
\curl_setopt($c, CURLOPT_POST, true);
|
||||||
|
} else {
|
||||||
|
\curl_setopt($c, CURLOPT_CUSTOMREQUEST, $method);
|
||||||
|
}
|
||||||
|
if (!\is_null($body)) {
|
||||||
|
\curl_setopt($c, CURLOPT_POSTFIELDS, $body);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
\curl_exec($c);
|
||||||
|
|
||||||
|
try {
|
||||||
|
$code = \curl_getinfo($c, CURLINFO_RESPONSE_CODE);
|
||||||
|
if (!$code) {
|
||||||
|
throw new \RuntimeException("Error " . \curl_errno($c) . ": " . \curl_error($c) . " for {$request_url}");
|
||||||
|
}
|
||||||
|
return new Response($request_url, $code, $this->response_headers, $this->response_body);
|
||||||
|
} finally {
|
||||||
|
\curl_close($c);
|
||||||
|
$this->response_headers = array();
|
||||||
|
$this->response_body = '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function fetchHeader($ch, $header)
|
||||||
|
{
|
||||||
|
$this->response_headers[] = \rtrim($header);
|
||||||
|
return \strlen($header);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function fetchData($ch, $data)
|
||||||
|
{
|
||||||
|
if ($this->max_response_kb) {
|
||||||
|
$data = \substr($data, 0, \min(\strlen($data), ($this->max_response_kb*1024) - \strlen($this->response_body)));
|
||||||
|
}
|
||||||
|
$this->response_body .= $data;
|
||||||
|
return \strlen($data);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function streamData($ch, $data)
|
||||||
|
{
|
||||||
|
return \fwrite($this->stream, $data);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,177 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace SnappyMail\HTTP\Request;
|
||||||
|
|
||||||
|
use \SnappyMail\HTTP\Response;
|
||||||
|
|
||||||
|
class Socket extends \SnappyMail\HTTP\Request
|
||||||
|
{
|
||||||
|
public function supportsSSL() : bool
|
||||||
|
{
|
||||||
|
return \function_exists('openssl_open');
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function __doRequest(string &$method, string &$request_url, &$body, array $extra_headers) : Response
|
||||||
|
{
|
||||||
|
$parts = \parse_url($request_url);
|
||||||
|
|
||||||
|
// Set a default port.
|
||||||
|
$port = 0;
|
||||||
|
if (\array_key_exists('port', $parts)) {
|
||||||
|
$port = $parts['port'];
|
||||||
|
} else if ('http' === $parts['scheme'] || 'https' === $parts['scheme']) {
|
||||||
|
$parts['port'] = self::getSchemePort($parts['scheme']);
|
||||||
|
} else {
|
||||||
|
throw new \RuntimeException("Scheme '{$parts['scheme']}' unsupported");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!\array_key_exists('path', $parts)) {
|
||||||
|
$parts['path'] = '/';
|
||||||
|
}
|
||||||
|
|
||||||
|
$headers = array(
|
||||||
|
"{$method} {$parts['path']}".(isset($parts['query']) ? "?{$parts['query']}" : '')." HTTP/1.1",
|
||||||
|
"Host: ".$parts['host'].($port ? ":".$port : ''),
|
||||||
|
"User-Agent: {$this->user_agent}",
|
||||||
|
'Connection: Close',
|
||||||
|
);
|
||||||
|
|
||||||
|
if ($extra_headers) {
|
||||||
|
$headers = \array_merge($headers, $extra_headers);
|
||||||
|
}
|
||||||
|
$headers = \implode("\r\n", $headers);
|
||||||
|
if (!\is_null($body)) {
|
||||||
|
if (!\stripos($headers,'Content-Type')) {
|
||||||
|
$headers .= "\r\nContent-Type: application/x-www-form-urlencoded";
|
||||||
|
}
|
||||||
|
$headers .= "\r\nContent-Length: ".\strlen($body);
|
||||||
|
}
|
||||||
|
|
||||||
|
$context = \stream_context_create();
|
||||||
|
if ('https' === $parts['scheme']) {
|
||||||
|
$parts['host'] = 'ssl://'.$parts['host'];
|
||||||
|
\stream_context_set_option($context, 'ssl', 'verify_peer_name', true);
|
||||||
|
if ($this->verify_peer || $this->ca_bundle) {
|
||||||
|
\stream_context_set_option($context, 'ssl', 'verify_peer', true);
|
||||||
|
if ($this->ca_bundle) {
|
||||||
|
if (\is_dir($this->ca_bundle) || (\is_link($this->ca_bundle) && \is_dir(\readlink($this->ca_bundle)))) {
|
||||||
|
\stream_context_set_option($context, 'ssl', 'capath', $this->ca_bundle);
|
||||||
|
} else {
|
||||||
|
\stream_context_set_option($context, 'ssl', 'cafile', $this->ca_bundle);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
\stream_context_set_option($context, 'ssl', 'allow_self_signed', true);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
$parts['host'] = 'tcp://'.$parts['host'];
|
||||||
|
}
|
||||||
|
|
||||||
|
$errno = 0;
|
||||||
|
$errstr = '';
|
||||||
|
|
||||||
|
$sock = \stream_socket_client("{$parts['host']}:{$parts['port']}", $errno, $errstr, $this->timeout, STREAM_CLIENT_CONNECT, $context);
|
||||||
|
if (false === $sock) {
|
||||||
|
throw new \RuntimeException($errstr);
|
||||||
|
}
|
||||||
|
|
||||||
|
\stream_set_timeout($sock, $this->timeout);
|
||||||
|
|
||||||
|
\fwrite($sock, $headers . "\r\n\r\n");
|
||||||
|
if (!\is_null($body)) {
|
||||||
|
\fwrite($sock, $body);
|
||||||
|
}
|
||||||
|
|
||||||
|
# Read all headers
|
||||||
|
$chunked = false;
|
||||||
|
$response_headers = array();
|
||||||
|
$data = \rtrim(\fgets($sock, 1024)); # read line
|
||||||
|
$code = \intval(\explode(' ', $data)[1]??0);
|
||||||
|
while (\strlen($data)) {
|
||||||
|
$response_headers[] = $data;
|
||||||
|
$chunked |= \preg_match('#Transfer-Encoding:.*chunked#i', $data);
|
||||||
|
|
||||||
|
if (401 === $code && $this->auth['user']) {
|
||||||
|
// Basic authentication
|
||||||
|
if ($this->auth['type'] & self::AUTH_BASIC && \preg_match("/WWW-Authenticate:\\s+Basic\\s+realm=([^\\r\\n]*)/i", $data, $match)) {
|
||||||
|
$extra_headers['Authorization'] = "Authorization: Basic " . \base64_encode($this->auth['user'] . ':' . $this->auth['pass']);
|
||||||
|
\fclose($sock);
|
||||||
|
return $this->__doRequest($method, $request_url, $body, $extra_headers);
|
||||||
|
}
|
||||||
|
// Digest authentication
|
||||||
|
else if ($this->auth['type'] & self::AUTH_DIGEST && \preg_match("/WWW-Authenticate:\\s+Digest\\s+([^\\r\\n]*)/i", $data, $match)) {
|
||||||
|
$challenge = [];
|
||||||
|
foreach (\split(',', $match[1]) as $i) {
|
||||||
|
$ii = \split('=', \trim($i), 2);
|
||||||
|
if (!empty($ii[1]) && !empty($ii[0])) {
|
||||||
|
$challenge[$ii[0]] = \preg_replace('/^"/','', \preg_replace('/"$/','', $ii[1]));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$a1 = \md5($this->auth['user'] . ':' . $challenge['realm'] . ':' . $this->auth['pass']);
|
||||||
|
$a2 = \md5($method . ':' . $request_url);
|
||||||
|
if (empty($challenge['qop'])) {
|
||||||
|
$digest = \md5($a1 . ':' . $challenge['nonce'] . ':' . $a2);
|
||||||
|
} else {
|
||||||
|
$challenge['cnonce'] = 'Req2.' . \random_int();
|
||||||
|
if (empty($challenge['nc'])) {
|
||||||
|
$challenge['nc'] = 1;
|
||||||
|
}
|
||||||
|
$nc = \sprintf('%08x', $challenge['nc']++);
|
||||||
|
$digest = \md5($a1 . ':' . $challenge['nonce'] . ':' . $nc . ':' . $challenge['cnonce'] . ':auth:' . $a2);
|
||||||
|
}
|
||||||
|
$extra_headers['Authorization'] = "Authorization: Digest "
|
||||||
|
. ' username="' . \str_replace(array('\\', '"'), array('\\\\', '\\"'), $this->auth['user']) . '",'
|
||||||
|
. ' realm="' . $challenge['realm'] . '",'
|
||||||
|
. ' nonce="' . $challenge['nonce'] . '",'
|
||||||
|
. ' uri="' . $request_url . '",'
|
||||||
|
. ' response="' . $digest . '"'
|
||||||
|
. (empty($challenge['opaque']) ? '' : ', opaque="' . $challenge['opaque'] . '"')
|
||||||
|
. (empty($challenge['qop']) ? '' : ', qop="auth", nc=' . $nc . ', cnonce="' . $challenge['cnonce'] . '"');
|
||||||
|
|
||||||
|
\fclose($sock);
|
||||||
|
return $this->__doRequest($method, $request_url, $body, $extra_headers);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$data = \rtrim(\fgets($sock, 1024)); # read next line
|
||||||
|
}
|
||||||
|
|
||||||
|
# Read body
|
||||||
|
$body = '';
|
||||||
|
if (\is_resource($this->stream)) {
|
||||||
|
while (!\feof($sock)) {
|
||||||
|
if ($chunked) {
|
||||||
|
$chunk = \hexdec(\trim(\fgets($sock, 8)));
|
||||||
|
if (!$chunk) { break; }
|
||||||
|
while ($chunk > 0) {
|
||||||
|
$tmp = \fread($sock, $chunk);
|
||||||
|
\fwrite($this->stream, $tmp);
|
||||||
|
$chunk -= \strlen($tmp);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
\fwrite($this->stream, \fread($sock, 1024));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
$max_bytes = $this->max_response_kb * 1024;
|
||||||
|
while (!\feof($sock) && (!$max_bytes || \strlen($body) < $max_bytes)) {
|
||||||
|
if ($chunked) {
|
||||||
|
$chunk = \hexdec(\trim(\fgets($sock, 8)));
|
||||||
|
if (!$chunk) { break; }
|
||||||
|
while ($chunk > 0) {
|
||||||
|
$tmp = \fread($sock, $chunk);
|
||||||
|
$body .= $tmp;
|
||||||
|
$chunk -= \strlen($tmp);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
$body .= \fread($sock, 1024);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
\fclose($sock);
|
||||||
|
|
||||||
|
return new Response($request_url, $code, $response_headers, $body);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,88 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace SnappyMail\HTTP;
|
||||||
|
|
||||||
|
class Response
|
||||||
|
{
|
||||||
|
public
|
||||||
|
$request_uri, # The URI that was passed to the fetcher
|
||||||
|
$final_uri; # The result of following redirects from the request_uri
|
||||||
|
protected
|
||||||
|
$status = 0, # The HTTP status code returned from the final_uri
|
||||||
|
$headers = array(), # The headers returned from the final_uri
|
||||||
|
$body; # The body returned from the final_uri
|
||||||
|
|
||||||
|
function __construct(string $request_uri, int $status = 0, array $headers = null, $body = null)
|
||||||
|
{
|
||||||
|
if ($headers) {
|
||||||
|
$name = null;
|
||||||
|
$this->headers = array();
|
||||||
|
foreach ($headers as $header) {
|
||||||
|
if (\strpos($header, ':')) {
|
||||||
|
list($name, $value) = \explode(':', $header, 2);
|
||||||
|
$name = \strtolower(\trim($name));
|
||||||
|
$value = \trim($value);
|
||||||
|
if (isset($this->headers[$name])) {
|
||||||
|
if (\is_array($this->headers[$name])) {
|
||||||
|
$this->headers[$name][] = $value;
|
||||||
|
} else {
|
||||||
|
$this->headers[$name] = array($this->headers[$name], $value);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
$this->headers[$name] = $value;
|
||||||
|
}
|
||||||
|
} else if ($name) {
|
||||||
|
// $this->headers[$name] .= \trim($header);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$this->request_uri = $request_uri;
|
||||||
|
$this->final_uri = $request_uri;
|
||||||
|
$this->status = (int) $status;
|
||||||
|
if (\function_exists('gzinflate') && isset($this->headers['content-encoding'])
|
||||||
|
&& (false !== \stripos($this->headers['content-encoding'], 'gzip'))) {
|
||||||
|
$this->body = \gzinflate(\substr($body, 10, -4));
|
||||||
|
} else {
|
||||||
|
$this->body = $body;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function __get($k)
|
||||||
|
{
|
||||||
|
return \property_exists($this, $k) ? $this->$k : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getHeader($names)
|
||||||
|
{
|
||||||
|
$names = \is_array($names) ? $names : array($names);
|
||||||
|
foreach ($names as $n) {
|
||||||
|
$n = \strtolower($n);
|
||||||
|
if (isset($this->headers[$n])) {
|
||||||
|
return $this->headers[$n];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getRedirectLocation() : ?string
|
||||||
|
{
|
||||||
|
if ($location = $this->getHeader('location')) {
|
||||||
|
$uri = \is_array($location) ? $location[0] : $location;
|
||||||
|
if (!\preg_match('#^[a-z][a-z0-9\\+\\.\\-]+://[^/]+#i', $uri)) {
|
||||||
|
// no host
|
||||||
|
\preg_match('#^([a-z][a-z0-9\\+\\.\\-]+://[^/]+)(/[^\\?\\#]*)#i', $this->final_uri, $match);
|
||||||
|
if ('/' === $uri[0]) {
|
||||||
|
// absolute path
|
||||||
|
$uri = $match[1] . $uri;
|
||||||
|
} else {
|
||||||
|
// relative path
|
||||||
|
$rpos = \strrpos($match[2], '/');
|
||||||
|
$uri = $match[1] . \substr($match[2], 0, $rpos+1) . $uri;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return $uri;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue