Bugfix: DAV getContactsPaths() failed due to failures of (string) SimpleXMLElement

Added better logging of DAV connection for #653 and #680
This commit is contained in:
the-djmaze 2022-11-17 11:27:29 +01:00
parent 28a70ccc8e
commit 28a7377f97
3 changed files with 87 additions and 102 deletions

View file

@ -35,13 +35,13 @@ trait Contacts
$oAccount = $this->getAccountFromToken(); $oAccount = $this->getAccountFromToken();
$oAddressBookProvider = $this->AddressBookProvider($oAccount); $oAddressBookProvider = $this->AddressBookProvider($oAccount);
if (!$oAddressBookProvider) { if (!$oAddressBookProvider) {
throw new \RainLoop\Exceptions\ClientException(\RainLoop\Notifications::ContactsSyncError); throw new \RainLoop\Exceptions\ClientException(\RainLoop\Notifications::ContactsSyncError, null, 'No AddressBookProvider');
} }
\ignore_user_abort(true); \ignore_user_abort(true);
\SnappyMail\HTTP\Stream::start(/*$binary = false*/); \SnappyMail\HTTP\Stream::start(/*$binary = false*/);
\SnappyMail\HTTP\Stream::JSON(['messsage'=>'start']); \SnappyMail\HTTP\Stream::JSON(['messsage'=>'start']);
if (!$oAddressBookProvider->Sync()) { if (!$oAddressBookProvider->Sync()) {
throw new \RainLoop\Exceptions\ClientException(\RainLoop\Notifications::ContactsSyncError); throw new \RainLoop\Exceptions\ClientException(\RainLoop\Notifications::ContactsSyncError, null, 'AddressBookProvider->Sync() failed');
} }
return $this->TrueResponse(__FUNCTION__); return $this->TrueResponse(__FUNCTION__);
} }

View file

@ -28,15 +28,11 @@ trait CardDAV
$aResponse = null; $aResponse = null;
try try
{ {
$this->oLogger->Write('PROPFIND '.$sPath, \LOG_INFO, 'DAV');
$aResponse = $oClient->propFind($sPath, array( $aResponse = $oClient->propFind($sPath, array(
'{DAV:}getlastmodified', '{DAV:}getlastmodified',
'{DAV:}resourcetype', '{DAV:}resourcetype',
'{DAV:}getetag' '{DAV:}getetag'
), 1); ), 1);
// $this->oLogger->WriteDump($aResponse);
} }
catch (\Throwable $oException) catch (\Throwable $oException)
{ {
@ -139,16 +135,12 @@ trait CardDAV
try try
{ {
$this->oLogger->Write('PROPFIND '.$sPath, \LOG_INFO, 'DAV');
$aResponse = $oClient->propFind($sPath, array( $aResponse = $oClient->propFind($sPath, array(
'{DAV:}current-user-principal', '{DAV:}current-user-principal',
'{DAV:}resourcetype', '{DAV:}resourcetype',
'{DAV:}displayname', '{DAV:}displayname',
'{urn:ietf:params:xml:ns:carddav}addressbook-home-set' '{urn:ietf:params:xml:ns:carddav}addressbook-home-set'
), 1); ), 1);
// $this->oLogger->WriteDump($aResponse);
} }
catch (\Throwable $oException) catch (\Throwable $oException)
{ {
@ -158,49 +150,47 @@ trait CardDAV
return $aResponse; return $aResponse;
} }
private function getContactsPaths(DAVClient $oClient, string $sUser, string $sPassword, string $sProxy = '') : array private function getContactsPaths(DAVClient $oClient, string $sPath, string $sUser, string $sPassword, string $sProxy = '') : array
{ {
$aContactsPaths = array(); $aContactsPaths = array();
$sCurrentUserPrincipal = ''; $sCurrentUserPrincipal = '';
$sAddressbookHomeSet = ''; $sAddressbookHomeSet = '';
// [{DAV:}current-user-principal] => /cloud/remote.php/carddav/principals/admin/ $aResponse = $this->detectionPropFind($oClient, '/.well-known/carddav')
// [{urn:ietf:params:xml:ns:carddav}addressbook-home-set] => /cloud/remote.php/carddav/addressbooks/admin/ ?: $this->detectionPropFind($oClient, $sPath);
$aResponse = $this->detectionPropFind($oClient, '/.well-known/carddav');
$sNextPath = ''; $sNextPath = '';
$sFirstNextPath = ''; $sFirstNextPath = '';
if (\is_array($aResponse)) if (\is_array($aResponse))
{ {
foreach ($aResponse as $sKey => $aItem) foreach ($aResponse as $sPropPath => $aItem)
{ {
if (empty($sAddressbookHomeSet) && !empty($aItem['{urn:ietf:params:xml:ns:carddav}addressbook-home-set']) && if (empty($sAddressbookHomeSet) && !empty($aItem['{urn:ietf:params:xml:ns:carddav}addressbook-home-set']['{DAV:}href'])
false === \strpos($aItem['{urn:ietf:params:xml:ns:carddav}addressbook-home-set'], '/calendar-proxy')) && false === \strpos($aItem['{urn:ietf:params:xml:ns:carddav}addressbook-home-set']['{DAV:}href'], '/calendar-proxy'))
{ {
$sAddressbookHomeSet = $aItem['{urn:ietf:params:xml:ns:carddav}addressbook-home-set']; $sAddressbookHomeSet = $aItem['{urn:ietf:params:xml:ns:carddav}addressbook-home-set']['{DAV:}href'];
continue; continue;
} }
if (empty($sCurrentUserPrincipal) && !empty($aItem['{DAV:}current-user-principal'])) if (empty($sCurrentUserPrincipal) && !empty($aItem['{DAV:}current-user-principal']['{DAV:}href']))
{ {
$sCurrentUserPrincipal = $aItem['{DAV:}current-user-principal']; $sCurrentUserPrincipal = $aItem['{DAV:}current-user-principal']['{DAV:}href'];
continue; continue;
} }
if (!empty($sKey)) if (!empty($sPropPath))
{ {
if (empty($sFirstNextPath)) if (empty($sFirstNextPath))
{ {
$sFirstNextPath = $sKey; $sFirstNextPath = $sPropPath;
} }
if (empty($sNextPath)) if (empty($sNextPath))
{ {
if (static::hasDAVCollection($aItem)) if (static::hasDAVCollection($aItem))
{ {
$sNextPath = $sKey; $sNextPath = $sPropPath;
continue; continue;
} }
} }
@ -219,36 +209,32 @@ trait CardDAV
{ {
return $aContactsPaths; return $aContactsPaths;
} }
else
{
if (\preg_match('/^http[s]?:\/\//i', $sNextPath)) if (\preg_match('/^http[s]?:\/\//i', $sNextPath))
{ {
$oClient = $this->getDavClientFromUrl($sNextPath, $sUser, $sPassword, $sProxy); $oClient = $this->getDavClientFromUrl($sNextPath, $sUser, $sPassword, $sProxy);
if ($oClient) if (!$oClient)
{
$sNextPath = $oClient->__UrlPath__;
}
else
{ {
return $aContactsPaths; return $aContactsPaths;
} }
$sNextPath = $oClient->__UrlPath__;
} }
if ($sPath != $sNextPath) {
$aResponse = $this->detectionPropFind($oClient, $sNextPath); $aResponse = $this->detectionPropFind($oClient, $sNextPath);
if (\is_array($aResponse)) if (\is_array($aResponse))
{ {
foreach ($aResponse as $sKey => $aItem) foreach ($aResponse as $aItem)
{ {
if (empty($sAddressbookHomeSet) && !empty($aItem['{urn:ietf:params:xml:ns:carddav}addressbook-home-set']) && if (empty($sAddressbookHomeSet) && !empty($aItem['{urn:ietf:params:xml:ns:carddav}addressbook-home-set']['{DAV:}href']) &&
false === \strpos($aItem['{urn:ietf:params:xml:ns:carddav}addressbook-home-set'], '/calendar-proxy')) false === \strpos($aItem['{urn:ietf:params:xml:ns:carddav}addressbook-home-set']['{DAV:}href'], '/calendar-proxy'))
{ {
$sAddressbookHomeSet = $aItem['{urn:ietf:params:xml:ns:carddav}addressbook-home-set']; $sAddressbookHomeSet = $aItem['{urn:ietf:params:xml:ns:carddav}addressbook-home-set']['{DAV:}href'];
continue; continue;
} }
if (empty($sCurrentUserPrincipal) && !empty($aItem['{DAV:}current-user-principal'])) if (empty($sCurrentUserPrincipal) && !empty($aItem['{DAV:}current-user-principal']['{DAV:}href']))
{ {
$sCurrentUserPrincipal = $aItem['{DAV:}current-user-principal']; $sCurrentUserPrincipal = $aItem['{DAV:}current-user-principal']['{DAV:}href'];
continue; continue;
} }
} }
@ -262,8 +248,6 @@ trait CardDAV
{ {
return $aContactsPaths; return $aContactsPaths;
} }
else
{
if (\preg_match('/^http[s]?:\/\//i', $sCurrentUserPrincipal)) if (\preg_match('/^http[s]?:\/\//i', $sCurrentUserPrincipal))
{ {
$oClient = $this->getDavClientFromUrl($sCurrentUserPrincipal, $sUser, $sPassword, $sProxy); $oClient = $this->getDavClientFromUrl($sCurrentUserPrincipal, $sUser, $sPassword, $sProxy);
@ -280,25 +264,22 @@ trait CardDAV
$aResponse = $this->detectionPropFind($oClient, $sCurrentUserPrincipal); $aResponse = $this->detectionPropFind($oClient, $sCurrentUserPrincipal);
if (\is_array($aResponse)) if (\is_array($aResponse))
{ {
foreach ($aResponse as $sKey => $aItem) foreach ($aResponse as $aItem)
{ {
if (empty($sAddressbookHomeSet) && !empty($aItem['{urn:ietf:params:xml:ns:carddav}addressbook-home-set']) && if (empty($sAddressbookHomeSet) && !empty($aItem['{urn:ietf:params:xml:ns:carddav}addressbook-home-set']['{DAV:}href']) &&
false === \strpos($aItem['{urn:ietf:params:xml:ns:carddav}addressbook-home-set'], '/calendar-proxy')) false === \strpos($aItem['{urn:ietf:params:xml:ns:carddav}addressbook-home-set']['{DAV:}href'], '/calendar-proxy'))
{ {
$sAddressbookHomeSet = $aItem['{urn:ietf:params:xml:ns:carddav}addressbook-home-set']; $sAddressbookHomeSet = $aItem['{urn:ietf:params:xml:ns:carddav}addressbook-home-set']['{DAV:}href'];
continue; continue;
} }
} }
} }
} }
}
if (empty($sAddressbookHomeSet)) if (empty($sAddressbookHomeSet))
{ {
return $aContactsPaths; return $aContactsPaths;
} }
else
{
if (\preg_match('/^http[s]?:\/\//i', $sAddressbookHomeSet)) if (\preg_match('/^http[s]?:\/\//i', $sAddressbookHomeSet))
{ {
$oClient = $this->getDavClientFromUrl($sAddressbookHomeSet, $sUser, $sPassword, $sProxy); $oClient = $this->getDavClientFromUrl($sAddressbookHomeSet, $sUser, $sPassword, $sProxy);
@ -315,13 +296,12 @@ trait CardDAV
$aResponse = $this->detectionPropFind($oClient, $sAddressbookHomeSet); $aResponse = $this->detectionPropFind($oClient, $sAddressbookHomeSet);
if (\is_array($aResponse)) if (\is_array($aResponse))
{ {
foreach ($aResponse as $sKey => $aItem) foreach ($aResponse as $sPropPath => $aItem)
{ {
if (!empty($sKey) && static::hasDAVCollection($aItem) if (!empty($sPropPath) && static::hasDAVCollection($aItem)
&& \in_array('{urn:ietf:params:xml:ns:carddav}addressbook', $aItem['{DAV:}resourcetype'])) && \in_array('{urn:ietf:params:xml:ns:carddav}addressbook', $aItem['{DAV:}resourcetype']))
{ {
$aContactsPaths[$sKey] = isset($aItem['{DAV:}displayname']) ? \trim($aItem['{DAV:}displayname']) : ''; $aContactsPaths[$sPropPath] = isset($aItem['{DAV:}displayname']) ? \trim($aItem['{DAV:}displayname']) : '';
}
} }
} }
} }
@ -329,6 +309,9 @@ trait CardDAV
return $aContactsPaths; return $aContactsPaths;
} }
/**
* Checks if remote path resourcetype is an addressbook
*/
private function checkContactsPath(DAVClient $oClient, string $sPath) : bool private function checkContactsPath(DAVClient $oClient, string $sPath) : bool
{ {
if (!$oClient) if (!$oClient)
@ -336,16 +319,12 @@ trait CardDAV
return false; return false;
} }
$this->oLogger->Write('PROPFIND '.$sPath, \LOG_INFO, 'DAV');
$aResponse = null; $aResponse = null;
try try
{ {
$aResponse = $oClient->propFind($sPath, array( $aResponse = $oClient->propFind($sPath, array(
'{DAV:}resourcetype' '{DAV:}resourcetype'
), 1); ), 1);
// $this->oLogger->WriteDump($aResponse);
} }
catch (\Throwable $oException) catch (\Throwable $oException)
{ {
@ -445,11 +424,13 @@ trait CardDAV
$bGood = true; $bGood = true;
if ('' === $sPath || '/' === $sPath || !$this->checkContactsPath($oClient, $sPath)) if ('' === $sPath || '/' === $sPath || !$this->checkContactsPath($oClient, $sPath))
{ {
$sNewPath = ''; /**
* Path is not an addressbook, try to find it
$aPaths = $this->getContactsPaths($oClient, $sUser, $sPassword, $sProxy); */
$aPaths = $this->getContactsPaths($oClient, $sPath, $sUser, $sPassword, $sProxy);
$this->oLogger->WriteDump($aPaths); $this->oLogger->WriteDump($aPaths);
$sNewPath = '';
if (\is_array($aPaths)) if (\is_array($aPaths))
{ {
if (1 < \count($aPaths)) if (1 < \count($aPaths))
@ -493,7 +474,7 @@ trait CardDAV
$sPath = $sNewPath; $sPath = $sNewPath;
$bGood = $this->checkContactsPath($oClient, $sPath); $bGood = $sPath && $this->checkContactsPath($oClient, $sPath);
} }
return $bGood ? $oClient : null; return $bGood ? $oClient : null;

View file

@ -41,7 +41,7 @@ class Client
$this->HTTP->setAuth(3, $settings['userName'] ?? '', $settings['password'] ?? ''); $this->HTTP->setAuth(3, $settings['userName'] ?? '', $settings['password'] ?? '');
$this->HTTP->max_response_kb = 0; $this->HTTP->max_response_kb = 0;
$this->HTTP->timeout = 15; // timeout in seconds. $this->HTTP->timeout = 15; // timeout in seconds.
// $this->HTTP->max_redirects = 0; $this->HTTP->max_redirects = 0;
} }
/** /**
@ -69,11 +69,12 @@ class Client
$url = $this->baseUri . $url; $url = $this->baseUri . $url;
} }
} }
\SnappyMail\Log::debug('DAV', "{$method} {$url}" . ($body ? "\n\t" . \str_replace("\n", "\n\t", $body) : ''));
$response = $this->HTTP->doRequest($method, $url, $body, $headers); $response = $this->HTTP->doRequest($method, $url, $body, $headers);
if (301 == $response->status) { if (301 == $response->status) {
// Like: RewriteRule ^\.well-known/carddav /nextcloud/remote.php/dav [R=301,L] // Like: RewriteRule ^\.well-known/carddav /nextcloud/remote.php/dav [R=301,L]
$location = $response->getRedirectLocation(); $location = $response->getRedirectLocation();
\trigger_error("Redirect {$url} to {$location}"); \SnappyMail\Log::info('DAV', "301 Redirect {$url} to {$location}");
$url = \preg_replace('@^(https?:)?//[^/]+[/$]@', '/', $location); $url = \preg_replace('@^(https?:)?//[^/]+[/$]@', '/', $location);
$parts = \parse_url($this->baseUri); $parts = \parse_url($this->baseUri);
$url = $parts['scheme'] . '://' . $parts['host'] . (isset($parts['port'])?':' . $parts['port']:'') . $url; $url = $parts['scheme'] . '://' . $parts['host'] . (isset($parts['port'])?':' . $parts['port']:'') . $url;
@ -82,6 +83,7 @@ class Client
if (300 <= $response->status) { if (300 <= $response->status) {
throw new \SnappyMail\HTTP\Exception("{$method} {$url}", $response->status, $response); throw new \SnappyMail\HTTP\Exception("{$method} {$url}", $response->status, $response);
} }
\SnappyMail\Log::debug('DAV', "{$response->status}: {$response->body}");
return $response; return $response;
} }
@ -104,23 +106,20 @@ class Client
*/ */
public function propFind(string $url, array $properties, int $depth = 0) : array public function propFind(string $url, array $properties, int $depth = 0) : array
{ {
$body = '<?xml version="1.0"?>' . "\n"; $body = '<?xml version="1.0"?>' . "\n" . '<d:propfind xmlns:d="DAV:"><d:prop>';
$body.= '<d:propfind xmlns:d="DAV:">' . "\n";
$body.= ' <d:prop>' . "\n";
foreach ($properties as $property) { foreach ($properties as $property) {
if (!\preg_match('/^{([^}]*)}(.*)$/', $property, $match)) { if (!\preg_match('/^{([^}]*)}(.*)$/', $property, $match)) {
throw new \InvalidArgumentException('\'' . $property . '\' is not a valid clark-notation formatted string'); throw new \InvalidArgumentException('\'' . $property . '\' is not a valid clark-notation formatted string');
} }
if ('DAV:' === $match[1]) { if ('DAV:' === $match[1]) {
$body .= " <d:{$match[2]} />\n"; $body .= "<d:{$match[2]}/>";
} else { } else {
$body .= " <x:{$match[2]} xmlns:x=\"{$match[1]}\"/>\n"; $body .= "<x:{$match[2]} xmlns:x=\"{$match[1]}\"/>";
} }
} }
$body .= ' </d:prop>' . "\n"; $body .= '</d:prop></d:propfind>';
$body .= '</d:propfind>';
$response = $this->request('PROPFIND', $url, $body, array( $response = $this->request('PROPFIND', $url, $body, array(
"Depth: {$depth}", "Depth: {$depth}",
@ -144,7 +143,7 @@ class Client
null, LIBXML_NOBLANKS | LIBXML_NOCDATA); null, LIBXML_NOBLANKS | LIBXML_NOCDATA);
if (false === $responseXML) { if (false === $responseXML) {
throw new \InvalidArgumentException('The passed data is not valid XML'); throw new \UnexpectedValueException("The passed data is not valid XML\n{$response->body}");
} }
$ns = \array_search('urn:DAV', $responseXML->getNamespaces(true)) ?: 'd'; $ns = \array_search('urn:DAV', $responseXML->getNamespaces(true)) ?: 'd';
@ -163,16 +162,21 @@ class Client
foreach ($propStat->xpath("{$ns}:prop") as $prop) { foreach ($propStat->xpath("{$ns}:prop") as $prop) {
foreach ($prop->xpath("*") as $element) { foreach ($prop->xpath("*") as $element) {
$propertyName = self::toClarkNotation($element); $propertyName = self::toClarkNotation($element);
if ('{DAV:}resourcetype' === $propertyName) {
$propList[$propertyName] = []; $propList[$propertyName] = [];
if ('{DAV:}resourcetype' === $propertyName) {
foreach ($element->xpath("*") as $resourcetype) { foreach ($element->xpath("*") as $resourcetype) {
$propList[$propertyName][] = self::toClarkNotation($resourcetype); $propList[$propertyName][] = self::toClarkNotation($resourcetype);
} }
} else { } else {
foreach ($element->xpath("*") as $child) {
$propList[$propertyName][self::toClarkNotation($child)] = (string) $child;
}
if (!$propList[$propertyName]) {
$propList[$propertyName] = (string) $element; $propList[$propertyName] = (string) $element;
} }
} }
} }
}
list($httpVersion, $statusCode, $message) = \explode(' ', $propStat->children('urn:DAV')->status, 3); list($httpVersion, $statusCode, $message) = \explode(' ', $propStat->children('urn:DAV')->status, 3);
$properties[$statusCode] = $propList; $properties[$statusCode] = $propList;
} }