Файловый менеджер - Редактировать - /home/digilove/public_html/administrator/components/com_akeeba/BackupEngine/Postproc/Connector/Davclient.php
Назад
<?php /** * Akeeba Engine * * @package akeebaengine * @copyright Copyright (c)2006-2023 Nicholas K. Dionysopoulos / Akeeba Ltd * @license GNU General Public License version 3, or later */ /** * SabreDAV DAV client * * This client wraps around Curl to provide a convenient API to a WebDAV * server. * * NOTE: This class is experimental, its api will likely change in the future. * * @copyright Copyright (C) 2007-2016 fruux GmbH (https://fruux.com/). * @author Evert Pot (http://evertpot.com/) * @license http://code.google.com/p/sabredav/wiki/License Modified BSD License * * Modified by Akeeba Ltd for use in Akeeba Backup in accordance with the aforementioned * license of the original source code. Original source code can be found at https://github.com/fruux/sabre-dav. */ namespace Akeeba\Engine\Postproc\Connector; defined('AKEEBAENGINE') || die(); use Akeeba\Engine\Factory; use Akeeba\Engine\Postproc\Connector\Davclient\Xml; use Akeeba\Engine\Postproc\ProxyAware; use Exception; use InvalidArgumentException; class Davclient { use ProxyAware; /** * Basic authentication */ public const AUTH_BASIC = 1; /** * Digest authentication */ public const AUTH_DIGEST = 2; /** * 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 = []; protected $headers = ''; protected $baseUri; protected $userName; protected $password; protected $proxy; protected $trustedCertificates; /** * 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 * * @throws InvalidArgumentException */ public function __construct(array $settings) { if (!isset($settings['baseUri'])) { throw new InvalidArgumentException('A baseUri must be provided'); } $validSettings = [ '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; } // We just need this class to unserialize a node collection. However in our case we really don't need it // since we are just checking if exists or not, we don't have to iterate on it, so we can use a (very) // simplified method just to avoid PHP warnings $this->propertyMap['{DAV:}resourcetype'] = Xml::class; } /** * 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) { [$namespace, $elementName] = Xml::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, [ '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 $result[200] ?? []; } $newResult = []; foreach ($result as $href => $statusList) { $newResult[$href] = $statusList[200] ?? []; } 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. * * @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) { [$namespace, $elementName] = Xml::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 . "\">"; } // 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, [ '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 []; } $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 * @throws Exception * */ public function request($method, $url = '', $body = null, $headers = []) { $this->headers = ''; Factory::getLog()->debug("Remote relative WebDav URL: " . $url); $url = $this->getAbsoluteUrl($url); // WebDAV requires spaces (AND SPACES ONLY!) to be encoded $url = str_replace(' ', '%20', $url); Factory::getLog()->debug("Absolute WebDav URL: " . $url); $curlSettings = [ CURLOPT_RETURNTRANSFER => true, // I can't get the headers in the response, since it would corrupt my downloads CURLOPT_HEADER => false, CURLOPT_HEADERFUNCTION => [$this, 'storeHeaders'], CURLOPT_POSTFIELDS => $body, // Automatically follow redirects CURLOPT_FOLLOWLOCATION => true, CURLOPT_MAXREDIRS => 5, CURLOPT_CAINFO => AKEEBA_CACERT_PEM, ]; if ($this->verifyPeer !== null) { $curlSettings[CURLOPT_SSL_VERIFYPEER] = $this->verifyPeer; } 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; case 'GET': $curlSettings[CURLOPT_CUSTOMREQUEST] = 'GET'; if (is_resource($body)) { $curlSettings[CURLOPT_FILE] = $body; unset($curlSettings[CURLOPT_POSTFIELDS]); } break; case 'PUT': // ATTENTION!!! If you want to upload a file, you have to directly supply the file content inside the request body. // This is necessary because if we hit a redirect, cURL will screw up since he can't automatically rewind // a file pointer resource, but he can do that with a string. $curlSettings[CURLOPT_CUSTOMREQUEST] = $method; // Some broken cURL versions cause an error. Forcing HTTP/1.1 seems to be fixing it. if (defined('CURLOPT_HTTP_VERSION') && defined('CURL_HTTP_VERSION_1_1')) { $curlSettings[CURLOPT_HTTP_VERSION] = CURL_HTTP_VERSION_1_1; } break; default: $curlSettings[CURLOPT_CUSTOMREQUEST] = $method; break; } // Adding HTTP headers // OwnCloud needs the the Content-Type header to be set $nHeaders = ['Content-Type: application/octet-stream']; 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; } [$response, $curlInfo, $curlErrNo, $curlError] = $this->curlRequest($url, $curlSettings); $this->parseHeaders(); $response = [ 'body' => $response, 'statusCode' => $curlInfo['http_code'], 'headers' => $this->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('Bad request', 400); case 401 : throw new Exception('Not authenticated', 401); case 402 : throw new Exception('Payment required', 402); case 403 : throw new Exception('Forbidden', 403); case 404: throw new Exception('Resource not found.', 404); case 405 : throw new Exception('Method not allowed', 405); case 409 : throw new Exception('Conflict', 409); case 412 : throw new Exception('Precondition failed', 412); case 413 : throw new Exception('Request Entity Too Large', 413); case 416 : throw new Exception('Requested Range Not Satisfiable', 416); case 500 : throw new Exception('Internal server error', 500); case 501 : throw new Exception('Not Implemented', 501); case 507 : throw new Exception('Insufficient storage', 507); default: throw new Exception('HTTP error response. (errorcode ' . $response['statusCode'] . ')'); } } return $response; } /** * 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 * @throws InvalidArgumentException * */ public function parseMultiStatus($body) { $body = Xml::convertDAVNamespace($body); $responseXML = simplexml_load_string($body, null, LIBXML_NOBLANKS | LIBXML_NOCDATA); if ($responseXML === false) { throw new InvalidArgumentException('The passed data is not valid XML'); } $responseXML->registerXPathNamespace('d', 'urn:DAV'); $propResult = []; foreach ($responseXML->xpath('d:response') as $response) { $response->registerXPathNamespace('d', 'urn:DAV'); $href = $response->xpath('d:href'); $href = (string) $href[0]; $properties = []; foreach ($response->xpath('d:propstat') as $propStat) { $propStat->registerXPathNamespace('d', 'urn:DAV'); $status = $propStat->xpath('d:status'); [$httpVersion, $statusCode, $message] = explode(' ', (string) $status[0], 3); // Only using the propertymap for results with status 200. $propertyMap = $statusCode === '200' ? $this->propertyMap : []; $properties[$statusCode] = Xml::parseProperties(dom_import_simplexml($propStat), $propertyMap); } $propResult[$href] = $properties; } return $propResult; } /** * Callback function used to collect all the headers. * Headers are important since it's the only way to detected * failures, but we can't add them to the response body since this would corrupt downloaded files (and doing a * substr on a potentially 100Mb file is not efficient) * * @param resource $ch Pointer to a cURL resource * @param string $ch_headers Header * * @return int Length of the header */ protected function storeHeaders($ch, $ch_headers) { $this->headers .= $ch_headers; return strlen($ch_headers); } /** * Parses the stored headers and updates the internal member with the last one for error detection * * @return void */ protected function parseHeaders() { // 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($this->headers, "\r\n")); // We only care about the last set of headers $headerBlob = $headerBlob[count($headerBlob) - 1]; // Splitting headers $headerBlob = explode("\r\n", $headerBlob); $headers = []; foreach ($headerBlob as $header) { $parts = explode(':', $header, 2); if (count($parts) == 2) { $headers[strtolower(trim($parts[0]))] = trim($parts[1]); } } $this->headers = $headers; } /** * 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 */ protected function curlRequest($url, $settings) { $curl = curl_init($url); $this->applyProxySettingsToCurl($curl); $followLocation = false; $maxRedirs = 5; if (isset($settings[CURLOPT_FOLLOWLOCATION])) { $followLocation = $settings[CURLOPT_FOLLOWLOCATION]; unset($settings[CURLOPT_FOLLOWLOCATION]); } if (isset($settings[CURLOPT_MAXREDIRS])) { $maxRedirs = $settings[CURLOPT_MAXREDIRS]; unset($settings[CURLOPT_MAXREDIRS]); } curl_setopt_array($curl, $settings); if ($followLocation) { @curl_setopt($curl, CURLOPT_FOLLOWLOCATION, 1); @curl_setopt($curl, CURLOPT_MAXREDIRS, $maxRedirs); } return [ curl_exec($curl), curl_getinfo($curl), curl_errno($curl), curl_error($curl), ]; } /** * 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; } $parts = parse_url($this->baseUri); $schemeAndHost = $parts['scheme'] . '://' . $parts['host']; $portString = isset($parts['port']) ? (':' . $parts['port']) : ''; $path = (isset($parts['path']) && !empty($parts['path'])) ? $parts['path'] : ''; $path = trim($path, '/') . '/'; $path = (strpos($path, '/') !== 0) ? ('/' . $path) : $path; return $schemeAndHost . ($portString) . $path . ltrim($url, '/'); } }
| ver. 1.4 |
Github
|
.
| PHP 7.4.33 | Генерация страницы: 0.01 |
proxy
|
phpinfo
|
Настройка