📁
SKYSHELL MANAGER
PHP v8.2.31
Create
Create
Path:
root
/
home
/
thevaxnx
/
nativize.com
/
staging
/
wp-includes
/
js
/
tinymce
/
themes
/
Name
Size
Perm
Actions
📁
inlite
-
0755
🗑️
🏷️
🔒
📁
modern
-
0755
🗑️
🏷️
🔒
📄
wp-links-opml.php
6.83 KB
0444
🗑️
🏷️
⬇️
✏️
🔒
Edit: CardDavClient.php
<?php /* * CardDAV client library for PHP ("PHP-CardDavClient"). * * Copyright (c) 2020-2021 Michael Stilkerich <ms@mike2k.de> * Licensed under the MIT license. See COPYING file in the project root for details. */ declare(strict_types=1); namespace MStilkerich\CardDavClient; use Psr\Http\Message\ResponseInterface as Psr7Response; use MStilkerich\CardDavClient\XmlElements\Prop; use MStilkerich\CardDavClient\XmlElements\Filter; use MStilkerich\CardDavClient\XmlElements\Multistatus; use MStilkerich\CardDavClient\XmlElements\ElementNames as XmlEN; use MStilkerich\CardDavClient\XmlElements\Deserializers; use MStilkerich\CardDavClient\Exception\XmlParseException; /** * Implements the operations of the CardDAV protocol. * * This class implements the lower level interactions with the CardDAV server that are utilized by the higher-level * operations offered by the public entities ({@see AddressbookCollection} etc.) and services ({@see Services\Sync}, * {@see Services\Discovery}. * * An application interacting with the carddavclient library should not interact with this class directly, and it is * considered an internal part of the library whose interfaces may change without being considered a change of the * library's API. * * @psalm-import-type HttpOptions from Account * @psalm-import-type RequestOptions from HttpClientAdapter * @psalm-import-type PropTypes from Prop * @package Internal\Communication */ class CardDavClient { private const MAP_NS2PREFIX = [ XmlEN::NSDAV => 'DAV', XmlEN::NSCARDDAV => 'CARDDAV', XmlEN::NSCS => 'CS', ]; /** @var string */ protected $base_uri; /** @var HttpClientAdapter */ protected $httpClient; /** * @psalm-param HttpOptions $httpOptions */ public function __construct(string $base_uri, array $httpOptions) { $this->base_uri = rtrim($base_uri, '/') . '/'; $this->httpClient = new HttpClientAdapterGuzzle($base_uri, $httpOptions); } /** * Requests a sync-collection REPORT from the CardDAV server. * * Note: Google's server does not accept an empty syncToken, though explicitly allowed for initial sync by RFC6578. * It will respond with 400 Bad Request and error message "Request contains an invalid argument." * * The Google issues have been reported to Google: https://issuetracker.google.com/issues/160190530 */ public function syncCollection(string $addressbookUri, string $syncToken): Multistatus { $srv = self::getParserService(); $body = $srv->write(XmlEN::REPORT_SYNCCOLL, [ XmlEN::SYNCTOKEN => $syncToken, XmlEN::SYNCLEVEL => "1", XmlEN::PROP => [ XmlEN::GETETAG => null ] ]); // RFC6578: Depth: 0 header is required for sync-collection report // Google requires a Depth: 1 header or the REPORT will only target the collection itself // This hack seems to be the simplest solution to behave RFC-compliant in general but have Google work // nonetheless if (strpos(self::concatUrl($this->base_uri, $addressbookUri), "www.googleapis.com") !== false) { $depthValue = "1"; } else { $depthValue = "0"; } $response = $this->httpClient->sendRequest('REPORT', $addressbookUri, [ "headers" => [ "Depth" => $depthValue, "Content-Type" => "application/xml; charset=UTF-8" ], "body" => $body ]); return self::checkAndParseXMLMultistatus($response); } public function getResource(string $uri): Psr7Response { $response = $this->httpClient->sendRequest('GET', $uri); self::assertHttpStatus($response, 200, 200, "GET $uri"); $body = (string) $response->getBody(); if (empty($body)) { throw new \Exception("Response to GET $uri request does not include a body"); } return $response; } /** * Fetches an address object. * * @param string $uri URI of the address object to fetch * @psalm-return array{etag: string, vcf: string} * @return array<string,string> * Associative array with keys * - etag (string): Entity tag of the created resource if returned by server, otherwise empty string. * - vcf (string): The address data of the address object */ public function getAddressObject(string $uri): array { $response = $this->getResource($uri); // presence of this header is required per RFC6352: // "A response to a GET request targeted at an address object resource MUST contain an ETag response header // field indicating the current value of the strong entity tag of the address object resource." $etag = $response->getHeaderLine("ETag"); if (empty($etag)) { throw new \Exception("Response to address object $uri GET request does not include ETag header"); } $body = (string) $response->getBody(); // checked to be present in getResource() return [ 'etag' => $etag, 'vcf' => $body ]; } /** * Requests the server to delete the given resource. */ public function deleteResource(string $uri): void { $response = $this->httpClient->sendRequest('DELETE', $uri); self::assertHttpStatus($response, 200, 204, "DELETE $uri"); } /** * Requests the server to update the given resource. * * Normally, the ETag of the existing expected server-side resource should be given to make the update * conditional on that no other changes have been done to the server-side resource, otherwise lost updates might * occur. However, if no ETag is given, the server-side resource is overwritten unconditionally. * * @return ?string * ETag of the updated resource, an empty string if no ETag was given by the server, or null if the update failed * because the server-side ETag did not match the given one. */ public function updateResource(string $body, string $uri, string $etag = ""): ?string { $headers = [ "Content-Type" => "text/vcard" ]; if (!empty($etag)) { $headers["If-Match"] = $etag; } $response = $this->httpClient->sendRequest( 'PUT', $uri, [ "headers" => $headers, "body" => $body ] ); $status = $response->getStatusCode(); if ($status == 412) { $etag = null; } else { self::assertHttpStatus($response, 200, 204, "PUT $uri"); $etag = $response->getHeaderLine("ETag"); } return $etag; } /** * Requests the server to create the given resource. * * On success, the actual URI of the new resource is contained in the returned array. * * @param string $body * The content of the newly created resource. * * @param string $suggestedUri * - If $post=false: The suggested new URI for the resource to create. If a resource by that name * already exists, names will be derived from this URI by appending a numerical suffix for a limited number of * retries. * - If $post=true: The "Add-Member" URI to perform the POST request to. The server chooses the URI of the new * resource. * * @param bool $post * If true, use a POST instead of a PUT request to create the resource (RFC 5995). * * @psalm-return array{uri: string, etag: string} * @return array<string,string> * Associative array with keys * - uri (string): URI of the new resource if the request was successful * - etag (string): Entity tag of the created resource if returned by server, otherwise empty string. */ public function createResource(string $body, string $suggestedUri, bool $post = false): array { $uri = $suggestedUri; $attempt = 0; $headers = [ "Content-Type" => "text/vcard" ]; if ($post) { $reqtype = 'POST'; $retryLimit = 1; } else { $reqtype = 'PUT'; // for PUT, we have to guess a free URI, so we give it several tries $retryLimit = 5; $headers["If-None-Match"] = "*"; } do { ++$attempt; $response = $this->httpClient->sendRequest( $reqtype, $uri, [ "headers" => $headers, "body" => $body ] ); $status = $response->getStatusCode(); // 201 -> New resource created // 200/204 -> Existing resource modified (should not happen b/c of If-None-Match // 412 -> Precondition failed if ($status == 412) { // make up a new random filename until retry limit is hit (append a random integer to the suggested // filename, e.g. /newcard.vcf could become /newcard-1234.vcf) $randint = rand(); $uri = preg_replace("/(\.[^.]*)?$/", "-$randint$0", $suggestedUri, 1); } } while (($status == 412) && ($attempt < $retryLimit)); self::assertHttpStatus($response, 201, 201, "$reqtype $suggestedUri"); $etag = $response->getHeaderLine("ETag"); if ($post) { $uri = $response->getHeaderLine("Location"); } return [ 'uri' => $uri, 'etag' => $etag ]; } /** * Issues an addressbook-multiget request to the server. * * @param string $addressbookUri URI of the addressbook to fetch the objects from * @psalm-param list<string> $requestedUris * @param array<int,string> $requestedUris * List of URIs of the objects to fetch * @psalm-param list<string> $requestedVCardProps * @param array<int,string> $requestedVCardProps * List of VCard properties to request, empty to request the full cards. * * @psalm-return Multistatus<XmlElements\ResponsePropstat> */ public function multiGet( string $addressbookUri, array $requestedUris, array $requestedVCardProps = [] ): Multistatus { $srv = self::getParserService(); // Determine the prop element for the report $reqprops = [ XmlEN::GETETAG => null, XmlEN::ADDRDATA => $this->determineReqCardProps($requestedVCardProps) ]; $body = $srv->write( XmlEN::REPORT_MULTIGET, array_merge( [ [ 'name' => XmlEN::PROP, 'value' => $reqprops ] ], array_map( function (string $uri): array { return [ 'name' => XmlEN::HREF, 'value' => $uri ]; }, $requestedUris ) ) ); $response = $this->httpClient->sendRequest('REPORT', $addressbookUri, [ "headers" => [ // RFC6352: Depth: 0 header is required for addressbook-multiget report. "Depth" => "0", "Content-Type" => "application/xml; charset=UTF-8" ], "body" => $body ]); return self::checkAndParseXMLMultistatus($response, XmlElements\ResponsePropstat::class); } /** * Issues an addressbook-query report. * * @param string $addressbookUri The URI of the addressbook collection to query * @param Filter $filter The query filter conditions * @psalm-param list<string> $requestedVCardProps * @param array<int,string> $requestedVCardProps * A list of the requested VCard properties. If empty array, the full VCards are requested from the server. * @param int $limit Tell the server to return at most $limit results. 0 means no limit. * @psalm-return Multistatus */ public function query( string $addressbookUri, Filter $filter, array $requestedVCardProps, int $limit ): Multistatus { $srv = self::getParserService(); $reportOptions = [ // requested properties (both WebDAV and VCard properties) [ 'name' => XmlEN::PROP, 'value' => [ XmlEN::GETETAG => null, XmlEN::ADDRDATA => $this->determineReqCardProps($requestedVCardProps) ] ], // filter element with the conditions that cards need to match [ 'name' => XmlEN::FILTER, 'attributes' => $filter->xmlAttributes(), 'value' => $filter ] ]; // Limit element if needed if ($limit > 0) { $reportOptions[] = [ 'name' => XmlEN::LIMIT, 'value' => [ 'name' => XmlEN::NRESULTS, 'value' => $limit ] ]; } $body = $srv->write(XmlEN::REPORT_QUERY, $reportOptions); $response = $this->httpClient->sendRequest('REPORT', $addressbookUri, [ "headers" => [ // RFC6352: Depth: 1 header sets query scope to the addressbook collection "Depth" => "1", "Content-Type" => "application/xml; charset=UTF-8" ], "body" => $body ]); return self::checkAndParseXMLMultistatus($response); } /** * Builds a CARDDAV::address-data element with the requested properties. * * If no properties are requested, returns null - an empty address-data element means that the full VCards shall be * returned. * * Some properties that are mandatory are added to the list. * * @psalm-param list<string> $requestedVCardProps * @param array<int,string> $requestedVCardProps List of the VCard properties requested by the user * @psalm-return null|list<array{name: string, attributes: array{name: string}}> * @return null|array<int, array<string, mixed>> */ private function determineReqCardProps(array $requestedVCardProps): ?array { if (empty($requestedVCardProps)) { return null; } $requestedVCardProps = self::addRequiredVCardProperties($requestedVCardProps); $reqprops = array_map( function (string $prop): array { return [ 'name' => XmlEN::VCFPROP, 'attributes' => [ 'name' => $prop ] ]; }, $requestedVCardProps ); return $reqprops; } /** * Retrieves a set of WebDAV properties for a resource. * * @param string $uri The URI of the resource to retrieve properties for. * @psalm-param list<string> $props * @param array<int,string> $props * List of properties to retrieve, given as XML element names * @psalm-param "0"|"1"|"infinity" $depth * @param string $depth Value for the Depth header * * @psalm-return list<array{uri: string, props: PropTypes}> * @return array<int, array<string,mixed>> */ public function findProperties( string $uri, array $props, string $depth = "0" ): array { $srv = self::getParserService(); $body = $srv->write(XmlEN::PROPFIND, [ XmlEN::PROP => array_fill_keys($props, null) ]); $result = $this->requestWithRedirectionTarget( 'PROPFIND', $uri, [ "headers" => [ // RFC4918: A client MUST submit a Depth header with a value of "0", "1", or "infinity" "Depth" => $depth, "Content-Type" => "application/xml; charset=UTF-8", // Prefer: reduce reply size if supported, see RFC8144 "Prefer" => "return=minimal" ], "body" => $body ] ); $multistatus = self::checkAndParseXMLMultistatus($result["response"], XmlElements\ResponsePropstat::class); $resultProperties = []; foreach ($multistatus->responses as $response) { $href = $response->href; // There may have been redirects involved in querying the properties, particularly during addressbook // discovery. They may even point to a different server than the original request URI. Return absolute URL // in the responses to allow the caller to know the actual location on that the properties where reported $respUri = self::concatUrl($result["location"], $href); if (!empty($response->propstat)) { foreach ($response->propstat as $propstat) { if (stripos($propstat->status, " 200 ") !== false) { $resultProperties[] = [ 'uri' => $respUri, 'props' => $propstat->prop->props ]; } } } } return $resultProperties; } /** * Adds required VCard properties to a set specified by the user. * * This is needed to ensure retrieval of a valid VCard, as some properties are mandatory. * * @psalm-param list<string> $requestedVCardProps * @param array<int,string> $requestedVCardProps List of properties requested by the user * @psalm-return list<string> * @return array<int,string> List of properties requested by the user, completed with mandatory properties. */ private static function addRequiredVCardProperties(array $requestedVCardProps): array { $minimumProps = [ 'BEGIN', 'END', 'FN', 'VERSION', 'UID' ]; foreach ($minimumProps as $prop) { if (!in_array($prop, $requestedVCardProps)) { $requestedVCardProps[] = $prop; } } return $requestedVCardProps; } private static function assertHttpStatus(Psr7Response $davReply, int $minCode, int $maxCode, string $nfo): void { $status = $davReply->getStatusCode(); if (($status < $minCode) || ($status > $maxCode)) { $reason = $davReply->getReasonPhrase(); $body = (string) $davReply->getBody(); throw new \Exception("$nfo HTTP request was not successful ($status $reason): $body"); } } /** * @template RT of XmlElements\Response * @psalm-param class-string<RT> $responseType * @psalm-return Multistatus<RT> * @return Multistatus */ private static function checkAndParseXMLMultistatus( Psr7Response $davReply, string $responseType = XmlElements\Response::class ): Multistatus { $multistatus = null; self::assertHttpStatus($davReply, 207, 207, "Expected Multistatus"); if (preg_match(';(?i)(text|application)/xml;', $davReply->getHeaderLine('Content-Type'))) { $service = self::getParserService(); $multistatus = $service->expect(XmlEN::MULTISTATUS, (string) $davReply->getBody()); } if (!($multistatus instanceof Multistatus)) { throw new XmlParseException("Response is not the expected Multistatus response."); } foreach ($multistatus->responses as $response) { if (!($response instanceof $responseType)) { throw new XmlParseException("Multistatus contains unexpected responses (Expected: $responseType)"); } } /** @psalm-var Multistatus<RT> */ return $multistatus; } /** * Performs a WebDAV request, automatically following redirections and providing the final target with the result. * * @param string $method The WebDAV method of the request (PROPFIND, REPORT, etc.) * @param string $uri The target of the request * @param RequestOptions $options Additional options for the request * * @psalm-return array{redirected: bool, location: string, response: Psr7Response} * @return array<string, mixed> */ private function requestWithRedirectionTarget(string $method, string $uri, array $options = []): array { $options['allow_redirects'] = false; $redirAttempt = 0; $redirLimit = 5; $uri = $this->absoluteUrl($uri); do { $response = $this->httpClient->sendRequest($method, $uri, $options); $scode = $response->getStatusCode(); // 301 Moved Permanently // 308 Permanent Redirect // 302 Found // 307 Temporary Redirect $isRedirect = (($scode == 301) || ($scode == 302) || ($scode == 307) || ($scode == 308)); if ($isRedirect && $response->hasHeader('Location')) { $uri = self::concatUrl($uri, $response->getHeaderLine('Location')); $redirAttempt++; } else { break; } } while ($redirAttempt < $redirLimit); return [ "redirected" => ($redirAttempt == 0), "location" => $uri, "response" => $response ]; } public function absoluteUrl(string $relurl): string { return self::concatUrl($this->base_uri, $relurl); } public static function concatUrl(string $baseurl, string $relurl): string { return \Sabre\Uri\resolve($baseurl, $relurl); } public static function compareUrlPaths(string $url1, string $url2): bool { $comp1 = \Sabre\Uri\parse($url1); $comp2 = \Sabre\Uri\parse($url2); $p1 = trim(rtrim($comp1["path"] ?? '', "/"), "/"); $p2 = trim(rtrim($comp2["path"] ?? '', "/"), "/"); return $p1 === $p2; } private static function getParserService(): \Sabre\Xml\Service { $service = new \Sabre\Xml\Service(); $service->namespaceMap = self::MAP_NS2PREFIX; $service->elementMap = array_merge( Prop::PROP_DESERIALIZERS, [ XmlEN::MULTISTATUS => XmlElements\Multistatus::class, XmlEN::RESPONSE => XmlElements\Response::class, XmlEN::PROPSTAT => XmlElements\Propstat::class, XmlEN::PROP => XmlElements\Prop::class, ] ); return $service; } } // vim: ts=4:sw=4:expandtab:fenc=utf8:ff=unix:tw=120
Save