@@ -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