Skip to content

Commit 736b3ff

Browse files
committed
feat(dav): refresh expired tokens
Signed-off-by: Enrique Pérez Arnaud <enrique@cazalla.net>
1 parent 2cf4f9c commit 736b3ff

1 file changed

Lines changed: 85 additions & 17 deletions

File tree

lib/private/Files/Storage/DAV.php

Lines changed: 85 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -323,6 +323,25 @@ protected function exchangeRefreshToken(): string {
323323
}
324324
}
325325

326+
/**
327+
* Check if bearer authentication is being used
328+
*/
329+
protected function isBearerAuth(): bool {
330+
return $this->authType !== null &&
331+
($this->authType & BearerAuthAwareSabreClient::AUTH_BEARER);
332+
}
333+
334+
/**
335+
* Reinitialize the client with a fresh access token
336+
* Used when the current bearer token has expired (401 response)
337+
*/
338+
protected function reinitWithFreshToken(): void {
339+
$this->logger->debug('Bearer token expired, refreshing token', ['app' => 'dav']);
340+
$this->ready = false;
341+
$this->password = ''; // Clear to force token exchange in init()
342+
$this->init();
343+
}
344+
326345
/**
327346
* Clear the stat cache
328347
*/
@@ -387,12 +406,13 @@ public function opendir(string $path) {
387406
* If not, request it from the server then store to cache.
388407
*
389408
* @param string $path path to propfind
409+
* @param bool $retryOnUnauthorized whether to retry on 401 response (used to prevent infinite loops)
390410
*
391411
* @return array|false propfind response or false if the entry was not found
392412
*
393413
* @throws ClientHttpException
394414
*/
395-
protected function propfind(string $path): array|false {
415+
protected function propfind(string $path, bool $retryOnUnauthorized = true): array|false {
396416
$path = $this->cleanPath($path);
397417
$cachedResponse = $this->statCache->get($path);
398418
// we either don't know it, or we know it exists but need more details
@@ -409,6 +429,9 @@ protected function propfind(string $path): array|false {
409429
if ($e->getHttpStatus() === 404 || $e->getHttpStatus() === 405) {
410430
$this->statCache->clear($path . '/');
411431
$this->statCache->set($path, false);
432+
} elseif ($e->getHttpStatus() === 401 && $retryOnUnauthorized && $this->isBearerAuth()) {
433+
$this->reinitWithFreshToken();
434+
return $this->propfind($path, false);
412435
} else {
413436
$this->convertException($e, $path);
414437
}
@@ -466,7 +489,7 @@ public function unlink(string $path): bool {
466489
return $result;
467490
}
468491

469-
public function fopen(string $path, string $mode) {
492+
public function fopen(string $path, string $mode, bool $retryOnUnauthorized = true) {
470493
$this->init();
471494
$path = $this->cleanPath($path);
472495
switch ($mode) {
@@ -492,6 +515,11 @@ public function fopen(string $path, string $mode) {
492515
if ($e->getResponse() instanceof ResponseInterface
493516
&& $e->getResponse()->getStatusCode() === 404) {
494517
return false;
518+
} elseif ($e->getResponse() instanceof ResponseInterface
519+
&& $e->getResponse()->getStatusCode() === 401
520+
&& $retryOnUnauthorized && $this->isBearerAuth()) {
521+
$this->reinitWithFreshToken();
522+
return $this->fopen($path, $mode, false);
495523
} else {
496524
throw $e;
497525
}
@@ -578,7 +606,7 @@ public function free_space(string $path): int|float|false {
578606
}
579607
}
580608

581-
public function touch(string $path, ?int $mtime = null): bool {
609+
public function touch(string $path, ?int $mtime = null, bool $retryOnUnauthorized = true): bool {
582610
$this->init();
583611
if (is_null($mtime)) {
584612
$mtime = time();
@@ -603,6 +631,10 @@ public function touch(string $path, ?int $mtime = null): bool {
603631
if ($e->getHttpStatus() === 501) {
604632
return false;
605633
}
634+
if ($e->getHttpStatus() === 401 && $retryOnUnauthorized && $this->isBearerAuth()) {
635+
$this->reinitWithFreshToken();
636+
return $this->touch($path, $mtime, false);
637+
}
606638
$this->convertException($e, $path);
607639
return false;
608640
} catch (\Exception $e) {
@@ -622,7 +654,7 @@ public function file_put_contents(string $path, mixed $data): int|float|false {
622654
return $result;
623655
}
624656

625-
protected function uploadFile(string $path, string $target): void {
657+
protected function uploadFile(string $path, string $target, bool $retryOnUnauthorized = true): void {
626658
$this->init();
627659

628660
// invalidate
@@ -636,20 +668,31 @@ protected function uploadFile(string $path, string $target): void {
636668
$auth = [];
637669
$headers = ['Authorization' => 'Bearer ' . $this->bearerToken];
638670
}
639-
$this->httpClientService
640-
->newClient()
641-
->put($this->createBaseUri() . $this->encodePath($target), [
642-
'body' => $source,
643-
'headers' => $headers,
644-
'auth' => $auth,
645-
// set upload timeout for users with slow connections or large files
646-
'timeout' => $this->timeout
647-
]);
671+
try {
672+
$this->httpClientService
673+
->newClient()
674+
->put($this->createBaseUri() . $this->encodePath($target), [
675+
'body' => $source,
676+
'headers' => $headers,
677+
'auth' => $auth,
678+
// set upload timeout for users with slow connections or large files
679+
'timeout' => $this->timeout
680+
]);
681+
} catch (\GuzzleHttp\Exception\ClientException $e) {
682+
if ($e->getResponse() instanceof ResponseInterface
683+
&& $e->getResponse()->getStatusCode() === 401
684+
&& $retryOnUnauthorized && $this->isBearerAuth()) {
685+
$this->reinitWithFreshToken();
686+
$this->uploadFile($path, $target, false);
687+
return;
688+
}
689+
throw $e;
690+
}
648691

649692
$this->removeCachedFile($target);
650693
}
651694

652-
public function rename(string $source, string $target): bool {
695+
public function rename(string $source, string $target, bool $retryOnUnauthorized = true): bool {
653696
$this->init();
654697
$source = $this->cleanPath($source);
655698
$target = $this->cleanPath($target);
@@ -674,13 +717,19 @@ public function rename(string $source, string $target): bool {
674717
$this->removeCachedFile($source);
675718
$this->removeCachedFile($target);
676719
return true;
720+
} catch (ClientHttpException $e) {
721+
if ($e->getHttpStatus() === 401 && $retryOnUnauthorized && $this->isBearerAuth()) {
722+
$this->reinitWithFreshToken();
723+
return $this->rename($source, $target, false);
724+
}
725+
$this->convertException($e);
677726
} catch (\Exception $e) {
678727
$this->convertException($e);
679728
}
680729
return false;
681730
}
682731

683-
public function copy(string $source, string $target): bool {
732+
public function copy(string $source, string $target, bool $retryOnUnauthorized = true): bool {
684733
$this->init();
685734
$source = $this->cleanPath($source);
686735
$target = $this->cleanPath($target);
@@ -702,6 +751,12 @@ public function copy(string $source, string $target): bool {
702751
$this->statCache->set($target, true);
703752
$this->removeCachedFile($target);
704753
return true;
754+
} catch (ClientHttpException $e) {
755+
if ($e->getHttpStatus() === 401 && $retryOnUnauthorized && $this->isBearerAuth()) {
756+
$this->reinitWithFreshToken();
757+
return $this->copy($source, $target, false);
758+
}
759+
$this->convertException($e);
705760
} catch (\Exception $e) {
706761
$this->convertException($e);
707762
}
@@ -802,11 +857,12 @@ protected function encodePath(string $path): string {
802857
}
803858

804859
/**
860+
* @param bool $retryOnUnauthorized whether to retry on 401 response (used to prevent infinite loops)
805861
* @return bool
806862
* @throws StorageInvalidException
807863
* @throws StorageNotAvailableException
808864
*/
809-
protected function simpleResponse(string $method, string $path, ?string $body, int $expected): bool {
865+
protected function simpleResponse(string $method, string $path, ?string $body, int $expected, bool $retryOnUnauthorized = true): bool {
810866
$path = $this->cleanPath($path);
811867
try {
812868
$response = $this->client->request($method, $this->encodePath($path), $body);
@@ -818,6 +874,11 @@ protected function simpleResponse(string $method, string $path, ?string $body, i
818874
return false;
819875
}
820876

877+
if ($e->getHttpStatus() === 401 && $retryOnUnauthorized && $this->isBearerAuth()) {
878+
$this->reinitWithFreshToken();
879+
return $this->simpleResponse($method, $path, $body, $expected, false);
880+
}
881+
821882
$this->convertException($e, $path);
822883
} catch (\Exception $e) {
823884
$this->convertException($e, $path);
@@ -974,7 +1035,7 @@ protected function convertException(Exception $e, string $path = ''): void {
9741035
// TODO: only log for now, but in the future need to wrap/rethrow exception
9751036
}
9761037

977-
public function getDirectoryContent(string $directory): \Traversable {
1038+
public function getDirectoryContent(string $directory, bool $retryOnUnauthorized = true): \Traversable {
9781039
$this->init();
9791040
$directory = $this->cleanPath($directory);
9801041
try {
@@ -996,6 +1057,13 @@ public function getDirectoryContent(string $directory): \Traversable {
9961057
$this->statCache->set($file, $response);
9971058
yield $this->getMetaFromPropfind($file, $response);
9981059
}
1060+
} catch (ClientHttpException $e) {
1061+
if ($e->getHttpStatus() === 401 && $retryOnUnauthorized && $this->isBearerAuth()) {
1062+
$this->reinitWithFreshToken();
1063+
yield from $this->getDirectoryContent($directory, false);
1064+
return;
1065+
}
1066+
$this->convertException($e, $directory);
9991067
} catch (\Exception $e) {
10001068
$this->convertException($e, $directory);
10011069
}

0 commit comments

Comments
 (0)