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

Handle queries resulting in downloads #1629

Merged
merged 3 commits into from
Apr 10, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
24 changes: 24 additions & 0 deletions src/app/services/actions/query-action-creator-util.ts
Original file line number Diff line number Diff line change
Expand Up @@ -245,3 +245,27 @@ export function parseResponse(
}
return response;
}

/**
* Check if query attempts to download from OneDrive's /content API or reporting API
* Examples:
* /drive/items/{item-id}/content
* /shares/{shareIdOrEncodedSharingUrl}/driveItem/content
* /me/drive/items/{item-id}/thumbnails/{thumb-id}/{size}/content
* /sites/{site-id}/drive/items/{item-id}/versions/{version-id}/content
* /reports/getOffice365ActivationCounts?$format=text/csv
* /reports/getEmailActivityUserCounts(period='D7')?$format=text/csv
* @param query
* @returns true if query calls the OneDrive or reporting API, otherwise false
*/
export function queryResultsInCorsError(sampleUrl: string): boolean {
sampleUrl = sampleUrl.toLowerCase();
if (
(['/drive/', '/drives/', '/driveItem/'].some((x) =>
sampleUrl.includes(x)) && sampleUrl.endsWith('/content')) ||
(sampleUrl.includes('/reports/') && sampleUrl.includes('$format=text/csv'))
) {
return true;
}
return false;
}
110 changes: 44 additions & 66 deletions src/app/services/actions/query-action-creators.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,19 +5,17 @@ import { IHistoryItem } from '../../../types/history';
import { IQuery } from '../../../types/query-runner';
import { IStatus } from '../../../types/status';
import { ClientError } from '../../utils/ClientError';
import { sanitizeQueryUrl } from '../../utils/query-url-sanitization';
import { parseSampleUrl } from '../../utils/sample-url-generation';
import { setStatusMessage } from '../../utils/status-message';
import { writeHistoryData } from '../../views/sidebar/history/history-utils';
import { CORS_ERROR_QUERIES } from '../graph-constants';
import {
anonymousRequest,
authenticatedRequest,
generateResponseDownloadUrl,
isFileResponse,
isImageResponse,
parseResponse,
queryResponse
queryResponse,
queryResultsInCorsError
} from './query-action-creator-util';
import { setQueryResponseStatus } from './query-status-action-creator';
import { addHistoryItem } from './request-history-action-creators';
Expand All @@ -31,26 +29,18 @@ export function runQuery(query: IQuery): Function {
if (tokenPresent) {
return authenticatedRequest(dispatch, query)
.then(async (response: Response) => {
await processResponse(
response,
respHeaders,
dispatch,
createdAt,
);
await processResponse(response, respHeaders, dispatch, createdAt);
})
.catch(async (error: any) => {
return handleError(dispatch, error);
});
}

return anonymousRequest(dispatch, query, getState)
.then(
async (response: Response) => {
await processResponse(
response,
respHeaders,
dispatch,
createdAt,
);
}
).catch(async (error: any) => {
.then(async (response: Response) => {
await processResponse(response, respHeaders, dispatch, createdAt);
})
.catch(async (error: any) => {
return handleError(dispatch, error);
});
};
Expand All @@ -59,7 +49,7 @@ export function runQuery(query: IQuery): Function {
response: Response,
respHeaders: any,
dispatch: Function,
createdAt: any,
createdAt: any
) {
let result = await parseResponse(response, respHeaders);
const duration = new Date().getTime() - new Date(createdAt).getTime();
Expand Down Expand Up @@ -93,8 +83,6 @@ export function runQuery(query: IQuery): Function {
status.ok = true;
status.messageType = MessageBarType.success;

dispatch(setQueryResponseStatus(status));

if (isFileResponse(respHeaders)) {
const contentDownloadUrl = await generateResponseDownloadUrl(
response,
Expand All @@ -106,59 +94,49 @@ export function runQuery(query: IQuery): Function {
};
}
}

return dispatch(
queryResponse({
body: result,
headers: respHeaders
})
);
} else {
const { requestUrl } = parseSampleUrl(sanitizeQueryUrl(query.sampleUrl));
// check if this is one of the queries that result in a CORS error
if (response.status === 0 && CORS_ERROR_QUERIES.has(requestUrl)) {
result = {
throwsCorsError: true,
workload: CORS_ERROR_QUERIES.get(requestUrl)
};
}

dispatch(
queryResponse({
body: result,
headers: respHeaders
})
);
return dispatch(setQueryResponseStatus(status));
}
}
}

function handleError(dispatch: Function, error: any) {
dispatch(
queryResponse({
body: error,
headers: null
})
);
if (error instanceof ClientError) {
dispatch(setQueryResponseStatus(status));

return dispatch(
setQueryResponseStatus({
messageType: MessageBarType.error,
ok: false,
status: 0,
statusText: `${error.name}: ${error.message}`
queryResponse({
body: result,
headers: respHeaders
})
);
}
return dispatch(
setQueryResponseStatus({

function handleError(dispatch: Function, error: any) {
let body = error;
const status: IStatus = {
messageType: MessageBarType.error,
ok: false,
status: 400,
statusText: 'Bad Request'
})
);
};

if (error instanceof ClientError) {
status.status = 0;
status.statusText = `${error.name}: ${error.message}`;
}

if (queryResultsInCorsError(query.sampleUrl)) {
status.status = 0;
status.statusText = 'CORS error';
body = {
throwsCorsError: true
};
}

dispatch(
queryResponse({
body,
headers: null
})
);

return dispatch(setQueryResponseStatus(status));
}
}

async function createHistory(
Expand Down
22 changes: 1 addition & 21 deletions src/app/services/graph-constants.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
export const GRAPH_URL = 'https://graph.microsoft.com';
export const GRAPH_API_VERSIONS = ['v1.0','beta'];
export const GRAPH_API_VERSIONS = ['v1.0', 'beta'];
export const USER_INFO_URL = `${GRAPH_URL}/v1.0/me`;
export const BETA_USER_INFO_URL = `${GRAPH_URL}/beta/me/profile`;
export const USER_PICTURE_URL = `${GRAPH_URL}/beta/me/photo/$value`;
Expand All @@ -21,28 +21,8 @@ export enum PERMS_SCOPE {
APPLICATION = 'Application',
PERSONAL = 'DelegatedPersonal'
}
export enum WORKLOAD {
ONEDRIVE = 'OneDrive',
O365REPORTING = 'O365Reporting'
}
export const ADAPTIVE_CARD_URL =
'https://templates.adaptivecards.io/graph.microsoft.com';
export const GRAPH_TOOOLKIT_EXAMPLE_URL = 'https://mgt.dev/?path=/story';
export const MOZILLA_CORS_DOCUMENTATION_LINK =
'https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS';
export const ONE_DRIVE_CONTENT_DOWNLOAD_DOCUMENTATION_LINK =
'https://docs.microsoft.com/en-us/graph/api/driveitem-get-content?view=graph-rest-1.0&tabs=http';
export const CORS_ERROR_QUERIES = new Map([
['groups/{groups-id}/drive/items/{items-id}/content', WORKLOAD.ONEDRIVE],
['sites/{sites-id}/drive/items/{items-id}/content', WORKLOAD.ONEDRIVE],
['users/{users-id}/drive/items/{items-id}/content', WORKLOAD.ONEDRIVE],
['drives/{drives-id}/items/{items-id}/content', WORKLOAD.ONEDRIVE],
['shares/{shares-id}/driveItem/content', WORKLOAD.ONEDRIVE],
['me/drive/items/{items-id}/content', WORKLOAD.ONEDRIVE],
['me/drive/root:<value>/content', WORKLOAD.ONEDRIVE],
['reports/getYammerGroupsActivityDetail(period=<value>)', WORKLOAD.O365REPORTING],
['reports/getTeamsDeviceUsageUserCounts(period=<value>)', WORKLOAD.O365REPORTING],
['reports/getSharePointSiteUsageDetail(period=<value>)',WORKLOAD.O365REPORTING],
['reports/getOneDriveUsageFileCounts(period=<value>)', WORKLOAD.O365REPORTING],
['reports/getEmailActivityUserCounts(period=<value>)', WORKLOAD.O365REPORTING]
]);
11 changes: 2 additions & 9 deletions src/app/views/app-sections/ResponseMessages.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,7 @@ import { IGraphResponse } from '../../../types/query-response';
import { IQuery } from '../../../types/query-runner';
import { runQuery } from '../../services/actions/query-action-creators';
import { setSampleQuery } from '../../services/actions/query-input-action-creators';
import {
MOZILLA_CORS_DOCUMENTATION_LINK,
ONE_DRIVE_CONTENT_DOWNLOAD_DOCUMENTATION_LINK,
WORKLOAD
} from '../../services/graph-constants';
import { MOZILLA_CORS_DOCUMENTATION_LINK } from '../../services/graph-constants';

interface ODataLink {
link: string;
Expand Down Expand Up @@ -73,14 +69,11 @@ export function responseMessages(graphResponse: IGraphResponse, sampleQuery: IQu

// Show CORS compliance message
if (body?.throwsCorsError) {
const documentationLink = body?.workload === WORKLOAD.ONEDRIVE
? ONE_DRIVE_CONTENT_DOWNLOAD_DOCUMENTATION_LINK
: MOZILLA_CORS_DOCUMENTATION_LINK;
return (
<div>
<MessageBar messageBarType={MessageBarType.warning}>
<FormattedMessage id={'Response content not available due to CORS policy'} />
<Link href={documentationLink}>
<Link target='_blank' href={MOZILLA_CORS_DOCUMENTATION_LINK}>
<FormattedMessage id={'here'} />
</Link>.
</MessageBar>
Expand Down