@@ -68,8 +68,8 @@ function createService(serviceName) {
68
68
* @return {string } The redirect URI.
69
69
*/
70
70
function getRedirectUri ( scriptId ) {
71
- return Utilities . formatString (
72
- 'https://script.google.com/macros/d/%s/ usercallback', scriptId ) ;
71
+ return 'https://script.google.com/macros/d/' + encodeURIComponent ( scriptId ) +
72
+ '/ usercallback';
73
73
}
74
74
75
75
if ( typeof module === 'object' ) {
@@ -459,25 +459,37 @@ Service_.prototype.handleCallback = function(callbackRequest) {
459
459
* otherwise.
460
460
*/
461
461
Service_ . prototype . hasAccess = function ( ) {
462
+ var token = this . getToken ( ) ;
463
+ if ( token && ! this . isExpired_ ( token ) ) return true ; // Token still has access.
464
+ var canGetToken = ( token && this . canRefresh_ ( token ) ) ||
465
+ this . privateKey_ || this . grantType_ ;
466
+ if ( ! canGetToken ) return false ;
467
+
462
468
return this . lockable_ ( function ( ) {
463
- var token = this . getToken ( ) ;
464
- if ( ! token || this . isExpired_ ( token ) ) {
465
- try {
466
- if ( token && this . canRefresh_ ( token ) ) {
467
- this . refresh ( ) ;
468
- } else if ( this . privateKey_ ) {
469
- this . exchangeJwt_ ( ) ;
470
- } else if ( this . grantType_ ) {
471
- this . exchangeGrant_ ( ) ;
472
- } else {
473
- return false ;
474
- }
475
- } catch ( e ) {
476
- this . lastError_ = e ;
469
+ // Get the token again, bypassing the local memory cache.
470
+ token = this . getToken ( true ) ;
471
+ // Check to see if the token is no longer missing or expired, as another
472
+ // execution may have refreshed it while we were waiting for the lock.
473
+ if ( token && ! this . isExpired_ ( token ) ) return true ; // Token now has access.
474
+ try {
475
+ if ( token && this . canRefresh_ ( token ) ) {
476
+ this . refresh ( ) ;
477
+ return true ;
478
+ } else if ( this . privateKey_ ) {
479
+ this . exchangeJwt_ ( ) ;
480
+ return true ;
481
+ } else if ( this . grantType_ ) {
482
+ this . exchangeGrant_ ( ) ;
483
+ return true ;
484
+ } else {
485
+ // This should never happen, since canGetToken should have been false
486
+ // earlier.
477
487
return false ;
478
488
}
489
+ } catch ( e ) {
490
+ this . lastError_ = e ;
491
+ return false ;
479
492
}
480
- return true ;
481
493
} ) ;
482
494
} ;
483
495
@@ -662,10 +674,13 @@ Service_.prototype.saveToken_ = function(token) {
662
674
663
675
/**
664
676
* Gets the token from the service's property store or cache.
677
+ * @param {boolean? } optSkipMemoryCheck If true, bypass the local memory cache
678
+ * when fetching the token.
665
679
* @return {Object } The token, or null if no token was found.
666
680
*/
667
- Service_ . prototype . getToken = function ( ) {
668
- return this . getStorage ( ) . getValue ( null ) ;
681
+ Service_ . prototype . getToken = function ( optSkipMemoryCheck ) {
682
+ // Gets the stored value under the null key, which is reserved for the token.
683
+ return this . getStorage ( ) . getValue ( null , optSkipMemoryCheck ) ;
669
684
} ;
670
685
671
686
/**
@@ -781,8 +796,8 @@ Service_.prototype.lockable_ = function(func) {
781
796
782
797
/**
783
798
* Obtain an access token using the custom grant type specified. Most often
784
- * this will be "client_credentials", in which case make sure to also specify an
785
- * Authorization header if required by your OAuth provider .
799
+ * this will be "client_credentials", and a client ID and secret are set an
800
+ * " Authorization: Basic ..." header will be added using those values .
786
801
*/
787
802
Service_ . prototype . exchangeGrant_ = function ( ) {
788
803
validate_ ( {
@@ -793,6 +808,20 @@ Service_.prototype.exchangeGrant_ = function() {
793
808
grant_type : this . grantType_
794
809
} ;
795
810
payload = extend_ ( payload , this . params_ ) ;
811
+
812
+ // For the client_credentials grant type, add a basic authorization header:
813
+ // - If the client ID and client secret are set.
814
+ // - No authorization header has been set yet.
815
+ var lowerCaseHeaders = toLowerCaseKeys_ ( this . tokenHeaders_ ) ;
816
+ if ( this . grantType_ === 'client_credentials' &&
817
+ this . clientId_ &&
818
+ this . clientSecret_ &&
819
+ ( ! lowerCaseHeaders || ! lowerCaseHeaders . authorization ) ) {
820
+ this . tokenHeaders_ = this . tokenHeaders_ || { } ;
821
+ this . tokenHeaders_ . authorization = 'Basic ' +
822
+ Utilities . base64Encode ( this . clientId_ + ':' + this . clientSecret_ ) ;
823
+ }
824
+
796
825
var token = this . fetchToken_ ( payload ) ;
797
826
this . saveToken_ ( token ) ;
798
827
} ;
@@ -839,25 +868,42 @@ function Storage_(prefix, properties, optCache) {
839
868
*/
840
869
Storage_ . CACHE_EXPIRATION_TIME_SECONDS = 21600 ; // 6 hours.
841
870
871
+ /**
872
+ * The special value to use in the cache to indicate that there is no value.
873
+ * @type {string }
874
+ * @private
875
+ */
876
+ Storage_ . CACHE_NULL_VALUE = '__NULL__' ;
877
+
842
878
/**
843
879
* Gets a stored value.
844
880
* @param {string } key The key.
881
+ * @param {boolean? } optSkipMemoryCheck Whether to bypass the local memory cache
882
+ * when fetching the value (the default is false).
845
883
* @return {* } The stored value.
846
884
*/
847
- Storage_ . prototype . getValue = function ( key ) {
848
- // Check memory.
849
- if ( this . memory_ [ key ] ) {
850
- return this . memory_ [ key ] ;
851
- }
852
-
885
+ Storage_ . prototype . getValue = function ( key , optSkipMemoryCheck ) {
853
886
var prefixedKey = this . getPrefixedKey_ ( key ) ;
854
887
var jsonValue ;
855
888
var value ;
856
889
890
+ if ( ! optSkipMemoryCheck ) {
891
+ // Check in-memory cache.
892
+ if ( value = this . memory_ [ key ] ) {
893
+ if ( value === Storage_ . CACHE_NULL_VALUE ) {
894
+ return null ;
895
+ }
896
+ return value ;
897
+ }
898
+ }
899
+
857
900
// Check cache.
858
901
if ( this . cache_ && ( jsonValue = this . cache_ . get ( prefixedKey ) ) ) {
859
902
value = JSON . parse ( jsonValue ) ;
860
903
this . memory_ [ key ] = value ;
904
+ if ( value === Storage_ . CACHE_NULL_VALUE ) {
905
+ return null ;
906
+ }
861
907
return value ;
862
908
}
863
909
@@ -872,7 +918,13 @@ Storage_.prototype.getValue = function(key) {
872
918
return value ;
873
919
}
874
920
875
- // Not found.
921
+ // Not found. Store a special null value in the memory and cache to reduce
922
+ // hits on the PropertiesService.
923
+ this . memory_ [ key ] = Storage_ . CACHE_NULL_VALUE ;
924
+ if ( this . cache_ ) {
925
+ this . cache_ . put ( prefixedKey , JSON . stringify ( Storage_ . CACHE_NULL_VALUE ) ,
926
+ Storage_ . CACHE_EXPIRATION_TIME_SECONDS ) ;
927
+ }
876
928
return null ;
877
929
} ;
878
930
@@ -998,6 +1050,25 @@ function extend_(destination, source) {
998
1050
return destination ;
999
1051
}
1000
1052
1053
+ /* exported toLowerCaseKeys_ */
1054
+ /**
1055
+ * Gets a copy of an object with all the keys converted to lower-case strings.
1056
+ *
1057
+ * @param {Object } obj The object to copy.
1058
+ * @return {Object } a shallow copy of the object with all lower-case keys.
1059
+ */
1060
+ function toLowerCaseKeys_ ( obj ) {
1061
+ if ( obj === null || typeof obj !== 'object' ) {
1062
+ return obj ;
1063
+ }
1064
+ // For each key in the source object, add a lower-case version to a new
1065
+ // object, and return it.
1066
+ return Object . keys ( obj ) . reduce ( function ( result , k ) {
1067
+ result [ k . toLowerCase ( ) ] = obj [ k ] ;
1068
+ return result ;
1069
+ } , { } ) ;
1070
+ }
1071
+
1001
1072
/****** code end *********/
1002
1073
; (
1003
1074
function copy ( src , target , obj ) {
0 commit comments