237 lines
8.3 KiB
TypeScript
237 lines
8.3 KiB
TypeScript
/**
|
|
* @license
|
|
* Copyright Google Inc. All Rights Reserved.
|
|
*
|
|
* Use of this source code is governed by an MIT-style license that can be
|
|
* found in the LICENSE file at https://angular.io/license
|
|
*/
|
|
|
|
import {DOCUMENT} from '@angular/common';
|
|
import {Inject, Injectable} from '@angular/core';
|
|
import {Observable, Observer} from 'rxjs';
|
|
|
|
import {HttpBackend, HttpHandler} from './backend';
|
|
import {HttpRequest} from './request';
|
|
import {HttpErrorResponse, HttpEvent, HttpEventType, HttpResponse} from './response';
|
|
|
|
// Every request made through JSONP needs a callback name that's unique across the
|
|
// whole page. Each request is assigned an id and the callback name is constructed
|
|
// from that. The next id to be assigned is tracked in a global variable here that
|
|
// is shared among all applications on the page.
|
|
let nextRequestId: number = 0;
|
|
|
|
// Error text given when a JSONP script is injected, but doesn't invoke the callback
|
|
// passed in its URL.
|
|
export const JSONP_ERR_NO_CALLBACK = 'JSONP injected script did not invoke callback.';
|
|
|
|
// Error text given when a request is passed to the JsonpClientBackend that doesn't
|
|
// have a request method JSONP.
|
|
export const JSONP_ERR_WRONG_METHOD = 'JSONP requests must use JSONP request method.';
|
|
export const JSONP_ERR_WRONG_RESPONSE_TYPE = 'JSONP requests must use Json response type.';
|
|
|
|
/**
|
|
* DI token/abstract type representing a map of JSONP callbacks.
|
|
*
|
|
* In the browser, this should always be the `window` object.
|
|
*
|
|
*
|
|
*/
|
|
export abstract class JsonpCallbackContext { [key: string]: (data: any) => void; }
|
|
|
|
/**
|
|
* Processes an `HttpRequest` with the JSONP method,
|
|
* by performing JSONP style requests.
|
|
* @see `HttpHandler`
|
|
* @see `HttpXhrBackend`
|
|
*
|
|
* @publicApi
|
|
*/
|
|
@Injectable()
|
|
export class JsonpClientBackend implements HttpBackend {
|
|
constructor(private callbackMap: JsonpCallbackContext, @Inject(DOCUMENT) private document: any) {}
|
|
|
|
/**
|
|
* Get the name of the next callback method, by incrementing the global `nextRequestId`.
|
|
*/
|
|
private nextCallback(): string { return `ng_jsonp_callback_${nextRequestId++}`; }
|
|
|
|
/**
|
|
* Processes a JSONP request and returns an event stream of the results.
|
|
* @param req The request object.
|
|
* @returns An observable of the response events.
|
|
*
|
|
*/
|
|
handle(req: HttpRequest<never>): Observable<HttpEvent<any>> {
|
|
// Firstly, check both the method and response type. If either doesn't match
|
|
// then the request was improperly routed here and cannot be handled.
|
|
if (req.method !== 'JSONP') {
|
|
throw new Error(JSONP_ERR_WRONG_METHOD);
|
|
} else if (req.responseType !== 'json') {
|
|
throw new Error(JSONP_ERR_WRONG_RESPONSE_TYPE);
|
|
}
|
|
|
|
// Everything else happens inside the Observable boundary.
|
|
return new Observable<HttpEvent<any>>((observer: Observer<HttpEvent<any>>) => {
|
|
// The first step to make a request is to generate the callback name, and replace the
|
|
// callback placeholder in the URL with the name. Care has to be taken here to ensure
|
|
// a trailing &, if matched, gets inserted back into the URL in the correct place.
|
|
const callback = this.nextCallback();
|
|
const url = req.urlWithParams.replace(/=JSONP_CALLBACK(&|$)/, `=${callback}$1`);
|
|
|
|
// Construct the <script> tag and point it at the URL.
|
|
const node = this.document.createElement('script');
|
|
node.src = url;
|
|
|
|
// A JSONP request requires waiting for multiple callbacks. These variables
|
|
// are closed over and track state across those callbacks.
|
|
|
|
// The response object, if one has been received, or null otherwise.
|
|
let body: any|null = null;
|
|
|
|
// Whether the response callback has been called.
|
|
let finished: boolean = false;
|
|
|
|
// Whether the request has been cancelled (and thus any other callbacks)
|
|
// should be ignored.
|
|
let cancelled: boolean = false;
|
|
|
|
// Set the response callback in this.callbackMap (which will be the window
|
|
// object in the browser. The script being loaded via the <script> tag will
|
|
// eventually call this callback.
|
|
this.callbackMap[callback] = (data?: any) => {
|
|
// Data has been received from the JSONP script. Firstly, delete this callback.
|
|
delete this.callbackMap[callback];
|
|
|
|
// Next, make sure the request wasn't cancelled in the meantime.
|
|
if (cancelled) {
|
|
return;
|
|
}
|
|
|
|
// Set state to indicate data was received.
|
|
body = data;
|
|
finished = true;
|
|
};
|
|
|
|
// cleanup() is a utility closure that removes the <script> from the page and
|
|
// the response callback from the window. This logic is used in both the
|
|
// success, error, and cancellation paths, so it's extracted out for convenience.
|
|
const cleanup = () => {
|
|
// Remove the <script> tag if it's still on the page.
|
|
if (node.parentNode) {
|
|
node.parentNode.removeChild(node);
|
|
}
|
|
|
|
// Remove the response callback from the callbackMap (window object in the
|
|
// browser).
|
|
delete this.callbackMap[callback];
|
|
};
|
|
|
|
// onLoad() is the success callback which runs after the response callback
|
|
// if the JSONP script loads successfully. The event itself is unimportant.
|
|
// If something went wrong, onLoad() may run without the response callback
|
|
// having been invoked.
|
|
const onLoad = (event: Event) => {
|
|
// Do nothing if the request has been cancelled.
|
|
if (cancelled) {
|
|
return;
|
|
}
|
|
|
|
// Cleanup the page.
|
|
cleanup();
|
|
|
|
// Check whether the response callback has run.
|
|
if (!finished) {
|
|
// It hasn't, something went wrong with the request. Return an error via
|
|
// the Observable error path. All JSONP errors have status 0.
|
|
observer.error(new HttpErrorResponse({
|
|
url,
|
|
status: 0,
|
|
statusText: 'JSONP Error',
|
|
error: new Error(JSONP_ERR_NO_CALLBACK),
|
|
}));
|
|
return;
|
|
}
|
|
|
|
// Success. body either contains the response body or null if none was
|
|
// returned.
|
|
observer.next(new HttpResponse({
|
|
body,
|
|
status: 200,
|
|
statusText: 'OK', url,
|
|
}));
|
|
|
|
// Complete the stream, the response is over.
|
|
observer.complete();
|
|
};
|
|
|
|
// onError() is the error callback, which runs if the script returned generates
|
|
// a Javascript error. It emits the error via the Observable error channel as
|
|
// a HttpErrorResponse.
|
|
const onError: any = (error: Error) => {
|
|
// If the request was already cancelled, no need to emit anything.
|
|
if (cancelled) {
|
|
return;
|
|
}
|
|
cleanup();
|
|
|
|
// Wrap the error in a HttpErrorResponse.
|
|
observer.error(new HttpErrorResponse({
|
|
error,
|
|
status: 0,
|
|
statusText: 'JSONP Error', url,
|
|
}));
|
|
};
|
|
|
|
// Subscribe to both the success (load) and error events on the <script> tag,
|
|
// and add it to the page.
|
|
node.addEventListener('load', onLoad);
|
|
node.addEventListener('error', onError);
|
|
this.document.body.appendChild(node);
|
|
|
|
// The request has now been successfully sent.
|
|
observer.next({type: HttpEventType.Sent});
|
|
|
|
// Cancellation handler.
|
|
return () => {
|
|
// Track the cancellation so event listeners won't do anything even if already scheduled.
|
|
cancelled = true;
|
|
|
|
// Remove the event listeners so they won't run if the events later fire.
|
|
node.removeEventListener('load', onLoad);
|
|
node.removeEventListener('error', onError);
|
|
|
|
// And finally, clean up the page.
|
|
cleanup();
|
|
};
|
|
});
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Identifies requests with the method JSONP and
|
|
* shifts them to the `JsonpClientBackend`.
|
|
*
|
|
* @see `HttpInterceptor`
|
|
*
|
|
* @publicApi
|
|
*/
|
|
@Injectable()
|
|
export class JsonpInterceptor {
|
|
constructor(private jsonp: JsonpClientBackend) {}
|
|
|
|
/**
|
|
* Identifies and handles a given JSONP request.
|
|
* @param req The outgoing request object to handle.
|
|
* @param next The next interceptor in the chain, or the backend
|
|
* if no interceptors remain in the chain.
|
|
* @returns An observable of the event stream.
|
|
*/
|
|
intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
|
|
if (req.method === 'JSONP') {
|
|
return this.jsonp.handle(req as HttpRequest<never>);
|
|
}
|
|
// Fall through for normal HTTP requests.
|
|
return next.handle(req);
|
|
}
|
|
}
|