@axa-fr/react-oidc 5.8.0-alpha0 → 5.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +31 -1
- package/dist/OidcSecure.d.ts +2 -1
- package/dist/OidcSecure.d.ts.map +1 -1
- package/dist/OidcSecure.js +5 -4
- package/dist/OidcSecure.js.map +1 -1
- package/dist/ReactOidc.d.ts +1 -2
- package/dist/ReactOidc.d.ts.map +1 -1
- package/dist/ReactOidc.js +4 -7
- package/dist/ReactOidc.js.map +1 -1
- package/dist/core/default-component/Callback.component.js +4 -4
- package/dist/core/default-component/Callback.component.js.map +1 -1
- package/dist/core/default-component/ServiceWorkerInstall.component.d.ts.map +1 -1
- package/dist/core/default-component/ServiceWorkerInstall.component.js +4 -7
- package/dist/core/default-component/ServiceWorkerInstall.component.js.map +1 -1
- package/dist/core/default-component/SilentCallback.component.d.ts.map +1 -1
- package/dist/core/default-component/SilentCallback.component.js +3 -5
- package/dist/core/default-component/SilentCallback.component.js.map +1 -1
- package/dist/vanilla/initSession.js +9 -9
- package/dist/vanilla/initSession.js.map +1 -1
- package/dist/vanilla/oidc.d.ts +8 -4
- package/dist/vanilla/oidc.d.ts.map +1 -1
- package/dist/vanilla/oidc.js +67 -102
- package/dist/vanilla/oidc.js.map +1 -1
- package/dist/vanilla/timer.d.ts.map +1 -1
- package/dist/vanilla/timer.js +7 -2
- package/dist/vanilla/timer.js.map +1 -1
- package/package.json +1 -1
- package/src/Home.tsx +5 -3
- package/src/MultiAuth.tsx +1 -2
- package/src/oidc/OidcSecure.tsx +9 -5
- package/src/oidc/ReactOidc.tsx +3 -6
- package/src/oidc/core/default-component/Callback.component.tsx +4 -4
- package/src/oidc/core/default-component/ServiceWorkerInstall.component.tsx +4 -7
- package/src/oidc/core/default-component/SilentCallback.component.tsx +3 -2
- package/src/oidc/vanilla/initSession.ts +9 -9
- package/src/oidc/vanilla/oidc.ts +48 -58
- package/src/oidc/vanilla/timer.ts +7 -3
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import React, {useEffect, useState, ComponentType} from 'react';
|
|
2
2
|
import AuthenticatingError from "./AuthenticateError.component";
|
|
3
|
-
import Oidc from "../../vanilla/oidc";
|
|
3
|
+
import Oidc, {getLoginParams} from "../../vanilla/oidc";
|
|
4
4
|
import Authenticating from "./Authenticating.component";
|
|
5
5
|
|
|
6
6
|
const ServiceWorkerInstall: ComponentType<any> = ({callBackError, authenticating, configurationName }) => {
|
|
@@ -15,12 +15,9 @@ const ServiceWorkerInstall: ComponentType<any> = ({callBackError, authenticating
|
|
|
15
15
|
let isMounted = true;
|
|
16
16
|
const playCallbackAsync = async () => {
|
|
17
17
|
try {
|
|
18
|
-
const
|
|
19
|
-
// @ts-ignore
|
|
20
|
-
get: (searchParams, prop) => searchParams.get(prop),
|
|
21
|
-
});
|
|
18
|
+
const loginParams = getLoginParams(configurationName)
|
|
22
19
|
// @ts-ignore
|
|
23
|
-
await getOidc(configurationName).loginAsync(
|
|
20
|
+
await getOidc(configurationName).loginAsync(loginParams.callbackPath, loginParams.extras,false, loginParams.state);
|
|
24
21
|
if(isMounted) {
|
|
25
22
|
setLoading(false);
|
|
26
23
|
}
|
|
@@ -32,7 +29,7 @@ const ServiceWorkerInstall: ComponentType<any> = ({callBackError, authenticating
|
|
|
32
29
|
}
|
|
33
30
|
};
|
|
34
31
|
playCallbackAsync();
|
|
35
|
-
return
|
|
32
|
+
return () => {
|
|
36
33
|
isMounted = false;
|
|
37
34
|
};
|
|
38
35
|
},[]);
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import React, {useEffect, ComponentType} from 'react';
|
|
2
|
-
import Oidc from "../../vanilla/oidc";
|
|
2
|
+
import Oidc, {getLoginParams} from "../../vanilla/oidc";
|
|
3
3
|
import {OidcSecure} from "../../OidcSecure";
|
|
4
4
|
|
|
5
5
|
const CallBack = ({configurationName}) =>{
|
|
@@ -23,7 +23,8 @@ const CallBack = ({configurationName}) =>{
|
|
|
23
23
|
}
|
|
24
24
|
|
|
25
25
|
const CallbackManager: ComponentType<any> = ({configurationName }) => {
|
|
26
|
-
|
|
26
|
+
const loginParams = getLoginParams(configurationName);
|
|
27
|
+
return <OidcSecure configurationName={configurationName} state={loginParams.state} extras={loginParams.extras}>
|
|
27
28
|
<CallBack configurationName={configurationName}/>
|
|
28
29
|
</OidcSecure>;
|
|
29
30
|
};
|
|
@@ -1,35 +1,35 @@
|
|
|
1
1
|
export const initSession = (configurationName) => {
|
|
2
2
|
|
|
3
3
|
const saveItemsAsync =(items) =>{
|
|
4
|
-
sessionStorage
|
|
4
|
+
sessionStorage[`oidc_items.${configurationName}`] = JSON.stringify(items);
|
|
5
5
|
return Promise.resolve();
|
|
6
6
|
}
|
|
7
7
|
|
|
8
8
|
const loadItemsAsync=() =>{
|
|
9
|
-
return Promise.resolve(JSON.parse(sessionStorage
|
|
9
|
+
return Promise.resolve(JSON.parse(sessionStorage[`oidc_items.${configurationName}`]));
|
|
10
10
|
}
|
|
11
11
|
|
|
12
12
|
const clearAsync=() =>{
|
|
13
|
-
sessionStorage[configurationName] = JSON.stringify({tokens:null});
|
|
13
|
+
sessionStorage[`oidc.${configurationName}`] = JSON.stringify({tokens:null});
|
|
14
14
|
return Promise.resolve();
|
|
15
15
|
}
|
|
16
16
|
|
|
17
17
|
const initAsync=async () => {
|
|
18
|
-
if(!sessionStorage[configurationName]){
|
|
19
|
-
sessionStorage[configurationName] = JSON.stringify({tokens:null});
|
|
18
|
+
if(!sessionStorage[`oidc.${configurationName}`]){
|
|
19
|
+
sessionStorage[`oidc.${configurationName}`] = JSON.stringify({tokens:null});
|
|
20
20
|
}
|
|
21
|
-
return Promise.resolve({ tokens : JSON.parse(sessionStorage[configurationName]).tokens });
|
|
21
|
+
return Promise.resolve({ tokens : JSON.parse(sessionStorage[`oidc.${configurationName}`]).tokens });
|
|
22
22
|
}
|
|
23
23
|
|
|
24
24
|
const setTokens = (tokens) => {
|
|
25
|
-
sessionStorage[configurationName] = JSON.stringify({tokens});
|
|
25
|
+
sessionStorage[`oidc.${configurationName}`] = JSON.stringify({tokens});
|
|
26
26
|
}
|
|
27
27
|
|
|
28
28
|
const getTokens = () => {
|
|
29
|
-
if(!sessionStorage[configurationName]){
|
|
29
|
+
if(!sessionStorage[`oidc.${configurationName}`]){
|
|
30
30
|
return null;
|
|
31
31
|
}
|
|
32
|
-
return JSON.stringify({ tokens : JSON.parse(sessionStorage[configurationName]).tokens });
|
|
32
|
+
return JSON.stringify({ tokens : JSON.parse(sessionStorage[`oidc.${configurationName}`]).tokens });
|
|
33
33
|
}
|
|
34
34
|
|
|
35
35
|
return { saveItemsAsync, loadItemsAsync, clearAsync, initAsync, setTokens, getTokens };
|
package/src/oidc/vanilla/oidc.ts
CHANGED
|
@@ -45,7 +45,7 @@ const extractAccessTokenPayload = tokens => {
|
|
|
45
45
|
}
|
|
46
46
|
const accessToken = tokens.accessToken;
|
|
47
47
|
try{
|
|
48
|
-
if (!accessToken || countLetter(accessToken,'.')
|
|
48
|
+
if (!accessToken || countLetter(accessToken,'.') != 2) {
|
|
49
49
|
return null;
|
|
50
50
|
}
|
|
51
51
|
return JSON.parse(atob(accessToken.split('.')[1]));
|
|
@@ -59,6 +59,11 @@ export interface StringMap {
|
|
|
59
59
|
[key: string]: string;
|
|
60
60
|
}
|
|
61
61
|
|
|
62
|
+
export interface loginCallbackResult {
|
|
63
|
+
state: string,
|
|
64
|
+
callbackPath: string,
|
|
65
|
+
}
|
|
66
|
+
|
|
62
67
|
export interface AuthorityConfiguration {
|
|
63
68
|
authorization_endpoint: string;
|
|
64
69
|
token_endpoint: string;
|
|
@@ -101,39 +106,36 @@ const loginCallbackWithAutoTokensRenewAsync = async (oidc) => {
|
|
|
101
106
|
}
|
|
102
107
|
oidc.publishEvent(Oidc.eventNames.token_aquired, oidc.tokens);
|
|
103
108
|
oidc.timeoutId = await autoRenewTokensAsync(oidc, tokens.refreshToken, oidc.tokens.expiresAt)
|
|
104
|
-
return response.state;
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
const renewTokenAsync = async (oidc, refreshToken, extras:StringMap=null) =>{
|
|
108
|
-
const tokens = await oidc.refreshTokensAsync(refreshToken, false, extras);
|
|
109
|
-
oidc.tokens= await setTokensAsync(oidc.serviceWorker, tokens);
|
|
110
|
-
if(!oidc.serviceWorker){
|
|
111
|
-
await oidc.session.setTokens(oidc.tokens);
|
|
112
|
-
}
|
|
113
|
-
if(!oidc.tokens){
|
|
114
|
-
return null;
|
|
115
|
-
}
|
|
116
|
-
oidc.publishEvent(Oidc.eventNames.token_renewed, oidc.tokens);
|
|
117
|
-
return oidc.tokens;
|
|
109
|
+
return { state:response.state, callbackPath : response.callbackPath };
|
|
118
110
|
}
|
|
119
111
|
|
|
120
112
|
const autoRenewTokensAsync = async (oidc, refreshToken, expiresAt) => {
|
|
121
113
|
const refreshTimeBeforeTokensExpirationInSecond = oidc.configuration.refresh_time_before_tokens_expiration_in_second ?? 60;
|
|
122
|
-
return
|
|
114
|
+
return timer.setTimeout(async () => {
|
|
123
115
|
const currentTimeUnixSecond = new Date().getTime() /1000;
|
|
124
116
|
const timeInfo = { timeLeft:((expiresAt - refreshTimeBeforeTokensExpirationInSecond)- currentTimeUnixSecond)};
|
|
125
117
|
oidc.publishEvent(Oidc.eventNames.token_timer, timeInfo);
|
|
126
118
|
if(currentTimeUnixSecond > (expiresAt - refreshTimeBeforeTokensExpirationInSecond)) {
|
|
127
|
-
const tokens = await
|
|
128
|
-
|
|
129
|
-
|
|
119
|
+
const tokens = await oidc.refreshTokensAsync(refreshToken);
|
|
120
|
+
oidc.tokens= await setTokensAsync(oidc.serviceWorker, tokens);
|
|
121
|
+
if(!oidc.serviceWorker){
|
|
122
|
+
await oidc.session.setTokens(oidc.tokens);
|
|
123
|
+
}
|
|
124
|
+
if(!oidc.tokens){
|
|
125
|
+
return;
|
|
130
126
|
}
|
|
127
|
+
oidc.publishEvent(Oidc.eventNames.token_renewed, oidc.tokens);
|
|
128
|
+
oidc.timeoutId = await autoRenewTokensAsync(oidc, tokens.refreshToken, oidc.tokens.expiresAt);
|
|
131
129
|
} else{
|
|
132
130
|
oidc.timeoutId = await autoRenewTokensAsync(oidc, refreshToken, expiresAt)
|
|
133
131
|
}
|
|
134
132
|
}, 1000);
|
|
135
133
|
}
|
|
136
134
|
|
|
135
|
+
export const getLoginParams = (configurationName) => {
|
|
136
|
+
return JSON.parse(sessionStorage[`oidc_login.${configurationName}`]);
|
|
137
|
+
}
|
|
138
|
+
|
|
137
139
|
const userInfoAsync = async (oidc) => {
|
|
138
140
|
if(oidc.userInfo != null){
|
|
139
141
|
return oidc.userInfo;
|
|
@@ -257,7 +259,8 @@ export class Oidc {
|
|
|
257
259
|
return oidcFactory(configuration, name);
|
|
258
260
|
}
|
|
259
261
|
static get(name="default") {
|
|
260
|
-
|
|
262
|
+
const insideBrowser = (typeof process === 'undefined');
|
|
263
|
+
if(!oidcDatabase.hasOwnProperty(name) && insideBrowser){
|
|
261
264
|
throw Error(`Oidc library does seem initialized.
|
|
262
265
|
Please checkout that you are using OIDC hook inside a <OidcProvider configurationName="${name}"></OidcProvider> compoment.`)
|
|
263
266
|
}
|
|
@@ -383,23 +386,24 @@ Please checkout that you are using OIDC hook inside a <OidcProvider configuratio
|
|
|
383
386
|
}
|
|
384
387
|
}
|
|
385
388
|
|
|
386
|
-
async loginAsync(callbackPath:string=undefined, extras:StringMap=null, installServiceWorker=true) {
|
|
389
|
+
async loginAsync(callbackPath:string=undefined, extras:StringMap=null, installServiceWorker=true, state:string=undefined) {
|
|
387
390
|
try {
|
|
388
391
|
const location = window.location;
|
|
389
392
|
const url = callbackPath || location.pathname + (location.search || '') + (location.hash || '');
|
|
390
|
-
const state = url;
|
|
391
393
|
this.publishEvent(eventNames.loginAsync_begin, {});
|
|
392
394
|
const configuration = this.configuration
|
|
393
395
|
// Security we cannot loggin from Iframe
|
|
394
396
|
if (!configuration.silent_redirect_uri && isInIframe()) {
|
|
395
397
|
throw new Error("Login from iframe is forbidden");
|
|
396
398
|
}
|
|
399
|
+
sessionStorage[`oidc_login.${this.configurationName}`] = JSON.stringify({callbackPath:url,extras,state});
|
|
400
|
+
|
|
397
401
|
let serviceWorker = await initWorkerAsync(configuration.service_worker_relative_url, this.configurationName);
|
|
398
402
|
const oidcServerConfiguration = await this.initAsync(configuration.authority, configuration.authority_configuration);
|
|
399
403
|
if(serviceWorker && installServiceWorker) {
|
|
400
|
-
const isServiceWorkerProxyActive = await serviceWorker.isServiceWorkerProxyActiveAsync()
|
|
404
|
+
const isServiceWorkerProxyActive = await serviceWorker.isServiceWorkerProxyActiveAsync();
|
|
401
405
|
if(!isServiceWorkerProxyActive) {
|
|
402
|
-
window.location.href = configuration.redirect_uri
|
|
406
|
+
window.location.href = `${configuration.redirect_uri}/service-worker-install`;
|
|
403
407
|
return;
|
|
404
408
|
}
|
|
405
409
|
}
|
|
@@ -424,13 +428,13 @@ Please checkout that you are using OIDC hook inside a <OidcProvider configuratio
|
|
|
424
428
|
extras: extras ?? configuration.extras
|
|
425
429
|
});
|
|
426
430
|
authorizationHandler.performAuthorizationRequest(oidcServerConfiguration, authRequest);
|
|
427
|
-
} catch(exception){
|
|
428
|
-
|
|
429
|
-
|
|
431
|
+
} catch(exception) {
|
|
432
|
+
this.publishEvent(eventNames.loginAsync_error, exception);
|
|
433
|
+
throw exception;
|
|
430
434
|
}
|
|
431
435
|
}
|
|
432
436
|
|
|
433
|
-
async loginCallbackAsync()
|
|
437
|
+
async loginCallbackAsync(){
|
|
434
438
|
try {
|
|
435
439
|
this.publishEvent(eventNames.loginCallbackAsync_begin, {});
|
|
436
440
|
const configuration = this.configuration;
|
|
@@ -452,7 +456,6 @@ Please checkout that you are using OIDC hook inside a <OidcProvider configuratio
|
|
|
452
456
|
const items = await session.loadItemsAsync();
|
|
453
457
|
storage = new MemoryStorageBackend(session.saveItemsAsync, items);
|
|
454
458
|
}
|
|
455
|
-
|
|
456
459
|
const promise = new Promise((resolve, reject) => {
|
|
457
460
|
const tokenHandler = new BaseTokenRequestHandler(new FetchRequestor());
|
|
458
461
|
// @ts-ignore
|
|
@@ -465,6 +468,7 @@ Please checkout that you are using OIDC hook inside a <OidcProvider configuratio
|
|
|
465
468
|
reject(error);
|
|
466
469
|
}
|
|
467
470
|
if (!response) {
|
|
471
|
+
reject("no response");
|
|
468
472
|
return;
|
|
469
473
|
}
|
|
470
474
|
|
|
@@ -490,7 +494,12 @@ Please checkout that you are using OIDC hook inside a <OidcProvider configuratio
|
|
|
490
494
|
|
|
491
495
|
try {
|
|
492
496
|
const tokenResponse = await tokenHandler.performTokenRequest(oidcServerConfiguration, tokenRequest);
|
|
493
|
-
|
|
497
|
+
const loginParams = getLoginParams(this.configurationName);
|
|
498
|
+
resolve({
|
|
499
|
+
tokens:tokenResponse,
|
|
500
|
+
state: request.state,
|
|
501
|
+
callbackPath : loginParams.callbackPath,
|
|
502
|
+
});
|
|
494
503
|
this.publishEvent(eventNames.loginCallbackAsync_end, {})
|
|
495
504
|
} catch(exception){
|
|
496
505
|
this.publishEvent(eventNames.loginCallbackAsync_error, exception);
|
|
@@ -508,20 +517,8 @@ Please checkout that you are using OIDC hook inside a <OidcProvider configuratio
|
|
|
508
517
|
}
|
|
509
518
|
|
|
510
519
|
}
|
|
511
|
-
|
|
512
|
-
async renewTokensAsync(extras:StringMap=null)
|
|
513
|
-
{
|
|
514
|
-
// @ts-ignore
|
|
515
|
-
if(this.tokens && this.tokens.refreshToken && this.timeoutId) {
|
|
516
|
-
// @ts-ignore
|
|
517
|
-
timer.clearTimeout(this.timeoutId);
|
|
518
|
-
// @ts-ignore
|
|
519
|
-
const tokens = await renewTokenAsync(this, this.tokens.refreshToken, extras);
|
|
520
|
-
await autoRenewTokensAsync(this, tokens.refreshToken, tokens.expiresAt);
|
|
521
|
-
}
|
|
522
|
-
}
|
|
523
520
|
|
|
524
|
-
async refreshTokensAsync(refreshToken, silentEvent = false
|
|
521
|
+
async refreshTokensAsync(refreshToken, silentEvent = false) {
|
|
525
522
|
const localSilentSigninAsync= async (exception=null) => {
|
|
526
523
|
try {
|
|
527
524
|
const silent_token_response = await this.silentSigninAsync();
|
|
@@ -549,17 +546,11 @@ Please checkout that you are using OIDC hook inside a <OidcProvider configuratio
|
|
|
549
546
|
}
|
|
550
547
|
const tokenHandler = new BaseTokenRequestHandler(new FetchRequestor());
|
|
551
548
|
|
|
552
|
-
let
|
|
553
|
-
if(
|
|
554
|
-
|
|
555
|
-
for (let [key, value] of Object.entries(extras)) {
|
|
556
|
-
extrasRequest[key] = value;
|
|
557
|
-
}
|
|
558
|
-
}
|
|
559
|
-
else if(configuration.token_request_extras) {
|
|
560
|
-
extrasRequest = {}
|
|
549
|
+
let extras = undefined;
|
|
550
|
+
if(configuration.token_request_extras) {
|
|
551
|
+
extras = {}
|
|
561
552
|
for (let [key, value] of Object.entries(configuration.token_request_extras)) {
|
|
562
|
-
|
|
553
|
+
extras[key] = value;
|
|
563
554
|
}
|
|
564
555
|
}
|
|
565
556
|
|
|
@@ -570,7 +561,7 @@ Please checkout that you are using OIDC hook inside a <OidcProvider configuratio
|
|
|
570
561
|
grant_type: GRANT_TYPE_REFRESH_TOKEN,
|
|
571
562
|
code: undefined,
|
|
572
563
|
refresh_token: refreshToken,
|
|
573
|
-
extras
|
|
564
|
+
extras
|
|
574
565
|
});
|
|
575
566
|
|
|
576
567
|
const oidcServerConfiguration = await this.initAsync(authority, configuration.authority_configuration);
|
|
@@ -583,7 +574,7 @@ Please checkout that you are using OIDC hook inside a <OidcProvider configuratio
|
|
|
583
574
|
}
|
|
584
575
|
}
|
|
585
576
|
|
|
586
|
-
loginCallbackWithAutoTokensRenewAsync():Promise<
|
|
577
|
+
loginCallbackWithAutoTokensRenewAsync():Promise<loginCallbackResult>{
|
|
587
578
|
return loginCallbackWithAutoTokensRenewAsync(this);
|
|
588
579
|
}
|
|
589
580
|
|
|
@@ -607,13 +598,12 @@ Please checkout that you are using OIDC hook inside a <OidcProvider configuratio
|
|
|
607
598
|
async logoutAsync(callbackPath: string | undefined = undefined, extras:StringMap=null) {
|
|
608
599
|
const configuration = this.configuration;
|
|
609
600
|
const oidcServerConfiguration = await this.initAsync(configuration.authority, configuration.authority_configuration);
|
|
610
|
-
// TODO implement real logout
|
|
611
601
|
if(callbackPath && (typeof callbackPath !== 'string'))
|
|
612
602
|
{
|
|
613
603
|
callbackPath = undefined;
|
|
614
604
|
console.warn('callbackPath path is not a string');
|
|
615
605
|
}
|
|
616
|
-
const path = callbackPath || location.pathname + (location.search || '') + (location.hash || '');
|
|
606
|
+
const path = (callbackPath === null || callbackPath === undefined) ? location.pathname + (location.search || '') + (location.hash || '') : callbackPath;
|
|
617
607
|
const url = window.location.origin +path;
|
|
618
608
|
// @ts-ignore
|
|
619
609
|
const idToken = this.tokens ? this.tokens.idToken : "";
|
|
@@ -634,4 +624,4 @@ Please checkout that you are using OIDC hook inside a <OidcProvider configuratio
|
|
|
634
624
|
}
|
|
635
625
|
|
|
636
626
|
|
|
637
|
-
export default Oidc;
|
|
627
|
+
export default Oidc;
|
|
@@ -62,7 +62,7 @@
|
|
|
62
62
|
} catch (error) {
|
|
63
63
|
return null;
|
|
64
64
|
}
|
|
65
|
-
|
|
65
|
+
const insideBrowser = (typeof process === 'undefined');
|
|
66
66
|
try {
|
|
67
67
|
if (SharedWorker) {
|
|
68
68
|
worker = new SharedWorker(blobURL);
|
|
@@ -70,7 +70,9 @@
|
|
|
70
70
|
}
|
|
71
71
|
} catch (error)
|
|
72
72
|
{
|
|
73
|
-
|
|
73
|
+
if(insideBrowser) {
|
|
74
|
+
console.warn("SharedWorker not available");
|
|
75
|
+
}
|
|
74
76
|
}
|
|
75
77
|
try {
|
|
76
78
|
if (Worker) {
|
|
@@ -79,7 +81,9 @@
|
|
|
79
81
|
}
|
|
80
82
|
} catch (error)
|
|
81
83
|
{
|
|
82
|
-
|
|
84
|
+
if(insideBrowser) {
|
|
85
|
+
console.warn("Worker not available");
|
|
86
|
+
}
|
|
83
87
|
}
|
|
84
88
|
|
|
85
89
|
return null;
|