diff --git a/snappymail/v/0.0.0/app/libraries/RainLoop/Providers/AddressBook/PdoAddressBook.php b/snappymail/v/0.0.0/app/libraries/RainLoop/Providers/AddressBook/PdoAddressBook.php index f8f1aed03..e1d0f391a 100644 --- a/snappymail/v/0.0.0/app/libraries/RainLoop/Providers/AddressBook/PdoAddressBook.php +++ b/snappymail/v/0.0.0/app/libraries/RainLoop/Providers/AddressBook/PdoAddressBook.php @@ -3,6 +3,7 @@ namespace RainLoop\Providers\AddressBook; use \RainLoop\Providers\AddressBook\Enumerations\PropertyType; +use \SnappyMail\DAV\Client as DAVClient; class PdoAddressBook extends \RainLoop\Common\PdoAbstract @@ -104,7 +105,7 @@ class PdoAddressBook return $aResult; } - private function prepearRemoteSyncData($oClient, string $sPath) + private function prepearRemoteSyncData(DAVClient $oClient, string $sPath) { $mResult = false; $aResponse = null; @@ -175,7 +176,7 @@ class PdoAddressBook 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(); @@ -214,7 +215,7 @@ class PdoAddressBook return $aResponse; } - private function detectionPropFind(\Sabre\DAV\Client $oClient, string $sPath) : ?array + private function detectionPropFind(DAVClient $oClient, string $sPath) : ?array { $aResponse = null; @@ -239,7 +240,7 @@ class PdoAddressBook 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(); @@ -249,11 +250,6 @@ class PdoAddressBook // [{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/ - if (!$oClient) - { - return $aContactsPaths; - } - $aResponse = $this->detectionPropFind($oClient, '/.well-known/carddav'); $sNextPath = ''; @@ -425,7 +421,7 @@ class PdoAddressBook return $aContactsPaths; } - private function checkContactsPath(\Sabre\DAV\Client $oClient, string $sPath) : bool + private function checkContactsPath(DAVClient $oClient, string $sPath) : bool { if (!$oClient) { @@ -475,7 +471,7 @@ class PdoAddressBook 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)) { @@ -509,7 +505,7 @@ class PdoAddressBook $aSettings['proxy'] = $sProxy; } - $oClient = new \Sabre\DAV\Client($aSettings); + $oClient = new DAVClient($aSettings); $oClient->setVerifyPeer(false); $oClient->__UrlPath__ = $aUrl['path']; @@ -519,13 +515,8 @@ class PdoAddressBook 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(); $sUserAddressBookNameName = ''; @@ -538,10 +529,6 @@ class PdoAddressBook } $oClient = $this->getDavClientFromUrl($sUrl, $sUser, $sPassword, $sProxy); - if (!$oClient) - { - return null; - } $sPath = $oClient->__UrlPath__; diff --git a/snappymail/v/0.0.0/app/libraries/Sabre/DAV/Client.php b/snappymail/v/0.0.0/app/libraries/Sabre/DAV/Client.php deleted file mode 100644 index 2d7ea4040..000000000 --- a/snappymail/v/0.0.0/app/libraries/Sabre/DAV/Client.php +++ /dev/null @@ -1,656 +0,0 @@ -$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 = '' . "\n"; - $body.= '' . "\n"; - $body.= ' ' . "\n"; - - foreach($properties as $property) { - - list( - $namespace, - $elementName - ) = XMLUtil::parseClarkNotation($property); - - if ($namespace === 'DAV:') { - $body.=' ' . "\n"; - } else { - $body.=" \n"; - } - - } - - $body.= ' ' . "\n"; - $body.= ''; - - $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 = '' . "\n"; - $body.= '' . "\n"; - - foreach($properties as $propName => $propValue) { - - list( - $namespace, - $elementName - ) = XMLUtil::parseClarkNotation($propName); - - if ($propValue === null) { - - $body.="\n"; - - if ($namespace === 'DAV:') { - $body.=' ' . "\n"; - } else { - $body.=" \n"; - } - - $body.="\n"; - - } else { - - $body.="\n"; - if ($namespace === 'DAV:') { - $body.=' '; - } else { - $body.=" "; - } - // Shitty.. i know - $body.=htmlspecialchars($propValue, ENT_NOQUOTES, 'UTF-8'); - if ($namespace === 'DAV:') { - $body.='' . "\n"; - } else { - $body.="\n"; - } - $body.="\n"; - - } - - } - - $body.= ''; - - $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; - - } - -} diff --git a/snappymail/v/0.0.0/app/libraries/Sabre/DAV/Exception.php b/snappymail/v/0.0.0/app/libraries/Sabre/DAV/Exception.php deleted file mode 100644 index af47c36a2..000000000 --- a/snappymail/v/0.0.0/app/libraries/Sabre/DAV/Exception.php +++ /dev/null @@ -1,64 +0,0 @@ -getAllowedMethods($server->getRequestUri()); - - return array( - 'Allow' => strtoupper(implode(', ',$methods)), - ); - - } - -} diff --git a/snappymail/v/0.0.0/app/libraries/Sabre/DAV/Exception/NotAuthenticated.php b/snappymail/v/0.0.0/app/libraries/Sabre/DAV/Exception/NotAuthenticated.php deleted file mode 100644 index 26eb61d3d..000000000 --- a/snappymail/v/0.0.0/app/libraries/Sabre/DAV/Exception/NotAuthenticated.php +++ /dev/null @@ -1,30 +0,0 @@ -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); - } - - } - -} diff --git a/snappymail/v/0.0.0/app/libraries/Sabre/DAV/Exception/RequestedRangeNotSatisfiable.php b/snappymail/v/0.0.0/app/libraries/Sabre/DAV/Exception/RequestedRangeNotSatisfiable.php deleted file mode 100644 index 2651b1d3e..000000000 --- a/snappymail/v/0.0.0/app/libraries/Sabre/DAV/Exception/RequestedRangeNotSatisfiable.php +++ /dev/null @@ -1,31 +0,0 @@ - - * 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 "]*)encoding="UTF-16"([^>]*)>|u','',$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; - - } - -} diff --git a/snappymail/v/0.0.0/app/libraries/snappymail/dav/client.php b/snappymail/v/0.0.0/app/libraries/snappymail/dav/client.php new file mode 100644 index 000000000..0c1e6c2ce --- /dev/null +++ b/snappymail/v/0.0.0/app/libraries/snappymail/dav/client.php @@ -0,0 +1,237 @@ + '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 = '' . "\n"; + $body.= '' . "\n"; + $body.= ' ' . "\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 .= " \n"; + } else { + $body .= " \n"; + } + } + + $body .= ' ' . "\n"; + $body .= ''; + + 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: + * + * 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; + } + +} diff --git a/snappymail/v/0.0.0/app/libraries/snappymail/http/exception.php b/snappymail/v/0.0.0/app/libraries/snappymail/http/exception.php new file mode 100644 index 000000000..bdc6c3df8 --- /dev/null +++ b/snappymail/v/0.0.0/app/libraries/snappymail/http/exception.php @@ -0,0 +1,84 @@ + '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); + } + +} diff --git a/snappymail/v/0.0.0/app/libraries/snappymail/http/request.php b/snappymail/v/0.0.0/app/libraries/snappymail/http/request.php new file mode 100644 index 000000000..fba6497b6 --- /dev/null +++ b/snappymail/v/0.0.0/app/libraries/snappymail/http/request.php @@ -0,0 +1,159 @@ + 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; + } +} diff --git a/snappymail/v/0.0.0/app/libraries/snappymail/http/request/curl.php b/snappymail/v/0.0.0/app/libraries/snappymail/http/request/curl.php new file mode 100644 index 000000000..313b83797 --- /dev/null +++ b/snappymail/v/0.0.0/app/libraries/snappymail/http/request/curl.php @@ -0,0 +1,115 @@ + $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); + } + +} diff --git a/snappymail/v/0.0.0/app/libraries/snappymail/http/request/socket.php b/snappymail/v/0.0.0/app/libraries/snappymail/http/request/socket.php new file mode 100644 index 000000000..93e4e809c --- /dev/null +++ b/snappymail/v/0.0.0/app/libraries/snappymail/http/request/socket.php @@ -0,0 +1,177 @@ +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); + } + +} diff --git a/snappymail/v/0.0.0/app/libraries/snappymail/http/response.php b/snappymail/v/0.0.0/app/libraries/snappymail/http/response.php new file mode 100644 index 000000000..1e6cffff9 --- /dev/null +++ b/snappymail/v/0.0.0/app/libraries/snappymail/http/response.php @@ -0,0 +1,88 @@ +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; + } + +}