Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

CORS workaround to enable downloading of content #1032

Merged
merged 22 commits into from
Oct 12, 2021
Merged
Show file tree
Hide file tree
Changes from 11 commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
2487624
CORS workaround to enable downloading of content
Jul 19, 2021
3421a3c
Merge branch 'dev' of https://github.com/microsoftgraph/microsoft-gra…
Jul 19, 2021
3bbddb9
Merge branch 'dev' of https://github.com/microsoftgraph/microsoft-gra…
Aug 4, 2021
196446b
Merge branch 'dev' of https://github.com/microsoftgraph/microsoft-gra…
Aug 29, 2021
44d12e1
Move response messages to own file
Sep 6, 2021
2efc4a7
Add method for checking if query response is downloadable
Sep 6, 2021
70f5d68
Have building of graph query and actual graph call in separate methods
Sep 8, 2021
ecf9f46
Merge branch 'dev' into task/cors-workaround
Sep 8, 2021
17fafd9
Show download link for failed redirect due to CORS
Sep 10, 2021
cd34524
Update messages
Sep 10, 2021
33321b2
Merge branch 'dev' into task/cors-workaround
millicentachieng Sep 15, 2021
4ab3469
Rename variable
Sep 17, 2021
1137eee
Merge branch 'dev' of https://github.com/microsoftgraph/microsoft-gra…
Sep 17, 2021
00bfdc1
Fix dispatch message for authenticated CORS workaround
Sep 17, 2021
5f870a3
Update messages
Sep 17, 2021
155fe48
Merge branch 'task/cors-workaround' of https://github.com/microsoftgr…
Sep 17, 2021
223aea2
Update messages to mention workaround is used because redirected down…
Sep 20, 2021
9240169
Update messages to mention workaround is used because redirected down…
Sep 20, 2021
f30b746
Merge branch 'dev' of https://github.com/microsoftgraph/microsoft-gra…
Sep 27, 2021
9e793ff
fix linting errors
Oct 12, 2021
fdb3e30
rename workflow
thewahome Oct 12, 2021
91f47be
Merge branch 'dev' into task/cors-workaround
thewahome Oct 12, 2021
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 20 additions & 12 deletions src/app/services/actions/profile-action-creators.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,17 @@
import { IUser } from '../../../types/profile';
import { IQuery } from '../../../types/query-runner';
import {
ACCOUNT_TYPE, BETA_USER_INFO_URL, DEFAULT_USER_SCOPES,
USER_INFO_URL, USER_PICTURE_URL
ACCOUNT_TYPE,
BETA_USER_INFO_URL,
DEFAULT_USER_SCOPES,
USER_INFO_URL,
USER_PICTURE_URL,
} from '../graph-constants';
import { PROFILE_REQUEST_ERROR, PROFILE_REQUEST_SUCCESS } from '../redux-constants';
import { makeRequest, parseResponse } from './query-action-creator-util';
import {
PROFILE_REQUEST_ERROR,
PROFILE_REQUEST_SUCCESS,
} from '../redux-constants';
import { makeGraphRequest, parseResponse } from './query-action-creator-util';
import { queryRunningStatus } from './query-loading-action-creators';

export function profileRequestSuccess(response: object): any {
Expand All @@ -24,13 +30,15 @@ export function profileRequestError(response: object): any {

const query: IQuery = {
selectedVerb: 'GET',
sampleHeaders: [{
name: 'Cache-Control',
value: 'no-cache'
}],
sampleHeaders: [
{
name: 'Cache-Control',
value: 'no-cache',
},
],
selectedVersion: '',
sampleUrl: ''
}
sampleUrl: '',
};

export function getProfileInfo(): Function {
return async (dispatch: Function) => {
Expand Down Expand Up @@ -93,10 +101,10 @@ async function getProfileResponse() {
const scopes = DEFAULT_USER_SCOPES.split(' ');
const respHeaders: any = {};

const response = await makeRequest(query.selectedVerb, scopes)(query);
const response = await makeGraphRequest(scopes)(query);
const userInfo = await parseResponse(response, respHeaders);
return {
userInfo,
response
response,
};
}
195 changes: 134 additions & 61 deletions src/app/services/actions/query-action-creator-util.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import {
AuthenticationHandlerOptions,
ResponseType
GraphRequest,
ResponseType,
} from '@microsoft/microsoft-graph-client';
import { MSALAuthenticationProviderOptions } from '@microsoft/microsoft-graph-client/lib/src/MSALAuthenticationProviderOptions';

Expand All @@ -21,7 +22,11 @@ export function queryResponse(response: object): IAction {
};
}

export async function anonymousRequest(dispatch: Function, query: IQuery, getState: Function) {
export async function anonymousRequest(
dispatch: Function,
query: IQuery,
getState: Function
) {
dispatch(queryRunningStatus(true));
const { proxyUrl } = getState();
const { graphUrl, options } = createAnonymousRequest(query, proxyUrl);
Expand All @@ -47,14 +52,76 @@ export function createAnonymousRequest(query: IQuery, proxyUrl: string) {
...sampleHeaders,
};

const options: IRequestOptions = { method: query.selectedVerb, headers };
const options: IRequestOptions = {
method: query.selectedVerb,
headers,
};
return { graphUrl, options };
}

export function authenticatedRequest(dispatch: Function, query: IQuery,
scopes: string[] = DEFAULT_USER_SCOPES.split(' ')) {
export function authenticatedRequest(
dispatch: Function,
query: IQuery,
scopes: string[] = DEFAULT_USER_SCOPES.split(' ')
) {
dispatch(queryRunningStatus(true));
return makeRequest(query.selectedVerb, scopes)(query);
return makeGraphRequest(scopes)(query);
}

function createAuthenticatedRequest(
scopes: string[],
query: IQuery
): GraphRequest {
const sampleHeaders: any = {};
sampleHeaders.SdkVersion = 'GraphExplorer/4.0';

if (query.sampleHeaders && query.sampleHeaders.length > 0) {
query.sampleHeaders.forEach((header) => {
sampleHeaders[header.name] = header.value;
});
}

const msalAuthOptions = new MSALAuthenticationProviderOptions(scopes);
const middlewareOptions = new AuthenticationHandlerOptions(
authProvider,
msalAuthOptions
);
const client = GraphClient.getInstance()
.api(encodeHashCharacters(query))
.middlewareOptions([middlewareOptions])
.headers(sampleHeaders)
.responseType(ResponseType.RAW);

return client;
}

export function makeGraphRequest(scopes: string[]): Function {
return async (query: IQuery) => {
let response;

const client = createAuthenticatedRequest(scopes, query);
millicentachieng marked this conversation as resolved.
Show resolved Hide resolved

switch (query.selectedVerb) {
case 'GET':
response = await client.get();
break;
case 'POST':
response = await client.post(query.sampleBody);
break;
case 'PUT':
response = await client.put(query.sampleBody);
break;
case 'PATCH':
response = await client.patch(query.sampleBody);
break;
case 'DELETE':
response = await client.delete();
break;
default:
return;
}
return Promise.resolve(response);
};
}

export function isImageResponse(contentType: string | undefined) {
Expand All @@ -70,19 +137,68 @@ export function isBetaURLResponse(json: any) {
return !!json?.account?.[0]?.source?.type?.[0];
}

export function getContentType(headers: Headers) {
const contentType = headers.get('content-type');
export function getContentType(headers: any) {
let contentType = null;

const contentTypes = headers['content-type'];
if (contentTypes) {
/* Example: application/json;odata.metadata=minimal;odata.streaming=true;IEEE754Compatible=false;charset=utf-8
* Take the first option after splitting since it is the only value useful in the description of the content
*/
const splitContentTypes = contentTypes.split(';');
if (splitContentTypes.length > 0) {
contentType = splitContentTypes[0].toLowerCase();
}
}
return contentType;
}

export function isFileResponse(headers: any) {
const contentDisposition = headers['content-disposition'];
if (contentDisposition) {
const directives = contentDisposition.split(';');
if (directives.contains('attachment')) {
return true;
}
}

// use content type to determine if response is file
const contentType = getContentType(headers);
if (contentType) {
const delimiterPos = contentType.indexOf(';');
if (delimiterPos !== -1) {
return contentType.substr(0, delimiterPos);
} else {
return contentType;
return (
contentType === 'application/octet-stream' ||
contentType === 'application/onenote' ||
contentType === 'application/pdf' ||
contentType.includes('application/vnd.') ||
contentType.includes('video/') ||
contentType.includes('audio/')
);
}
return false;
}

export async function generateResponseDownloadUrl(
response: Response,
respHeaders: any
) {
try {
const fileContents = await parseResponse(response, respHeaders);
const contentType = getContentType(respHeaders);
if (fileContents) {
const buffer = await response.arrayBuffer();
const blob = new Blob([buffer], { type: contentType });
const downloadUrl = URL.createObjectURL(blob);
return downloadUrl;
}
} catch (error) {
return null;
}
}

export function parseResponse(response: any, respHeaders: any): Promise<any> {
export function parseResponse(
response: any,
respHeaders: any = {}
): Promise<any> {
if (response && response.headers) {
response.headers.forEach((val: any, key: any) => {
respHeaders[key] = val;
Expand All @@ -105,50 +221,7 @@ export function parseResponse(response: any, respHeaders: any): Promise<any> {
return response;
}

export function makeRequest(httpVerb: string, scopes: string[]): Function {
return async (query: IQuery) => {
const sampleHeaders: any = {};
sampleHeaders.SdkVersion = 'GraphExplorer/4.0';

if (query.sampleHeaders && query.sampleHeaders.length > 0) {
query.sampleHeaders.forEach((header) => {
sampleHeaders[header.name] = header.value;
});
}

const msalAuthOptions = new MSALAuthenticationProviderOptions(scopes);
const middlewareOptions = new AuthenticationHandlerOptions(
authProvider,
msalAuthOptions
);
const client = GraphClient.getInstance()
.api(encodeHashCharacters(query))
.middlewareOptions([middlewareOptions])
.headers(sampleHeaders)
.responseType(ResponseType.RAW);

let response;

switch (httpVerb) {
case 'GET':
response = await client.get();
break;
case 'POST':
response = await client.post(query.sampleBody);
break;
case 'PUT':
response = await client.put(query.sampleBody);
break;
case 'PATCH':
response = await client.patch(query.sampleBody);
break;
case 'DELETE':
response = await client.delete();
break;
default:
return;
}

return Promise.resolve(response);
};
};
export function queryResultsInCorsError(sampleQuery: IQuery) {
const requestUrl = new URL(sampleQuery.sampleUrl);
return requestUrl.pathname.match(/\/content(\/)*$/i) != null;
}
Loading