/** * @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 */ /** * A codec for encoding and decoding parameters in URLs. * * Used by `HttpParams`. * * @publicApi **/ export interface HttpParameterCodec { encodeKey(key: string): string; encodeValue(value: string): string; decodeKey(key: string): string; decodeValue(value: string): string; } /** * Provides encoding and decoding of URL parameter and query-string values. * * Serializes and parses URL parameter keys and values to encode and decode them. * If you pass URL query parameters without encoding, * the query parameters can be misinterpreted at the receiving end. * * * @publicApi */ export class HttpUrlEncodingCodec implements HttpParameterCodec { /** * Encodes a key name for a URL parameter or query-string. * @param key The key name. * @returns The encoded key name. */ encodeKey(key: string): string { return standardEncoding(key); } /** * Encodes the value of a URL parameter or query-string. * @param value The value. * @returns The encoded value. */ encodeValue(value: string): string { return standardEncoding(value); } /** * Decodes an encoded URL parameter or query-string key. * @param key The encoded key name. * @returns The decoded key name. */ decodeKey(key: string): string { return decodeURIComponent(key); } /** * Decodes an encoded URL parameter or query-string value. * @param value The encoded value. * @returns The decoded value. */ decodeValue(value: string) { return decodeURIComponent(value); } } function paramParser(rawParams: string, codec: HttpParameterCodec): Map { const map = new Map(); if (rawParams.length > 0) { const params: string[] = rawParams.split('&'); params.forEach((param: string) => { const eqIdx = param.indexOf('='); const [key, val]: string[] = eqIdx == -1 ? [codec.decodeKey(param), ''] : [codec.decodeKey(param.slice(0, eqIdx)), codec.decodeValue(param.slice(eqIdx + 1))]; const list = map.get(key) || []; list.push(val); map.set(key, list); }); } return map; } function standardEncoding(v: string): string { return encodeURIComponent(v) .replace(/%40/gi, '@') .replace(/%3A/gi, ':') .replace(/%24/gi, '$') .replace(/%2C/gi, ',') .replace(/%3B/gi, ';') .replace(/%2B/gi, '+') .replace(/%3D/gi, '=') .replace(/%3F/gi, '?') .replace(/%2F/gi, '/'); } interface Update { param: string; value?: string; op: 'a'|'d'|'s'; } /** Options used to construct an `HttpParams` instance. * * @publicApi */ export interface HttpParamsOptions { /** * String representation of the HTTP parameters in URL-query-string format. * Mutually exclusive with `fromObject`. */ fromString?: string; /** Object map of the HTTP parameters. Mutually exclusive with `fromString`. */ fromObject?: {[param: string]: string | ReadonlyArray}; /** Encoding codec used to parse and serialize the parameters. */ encoder?: HttpParameterCodec; } /** * An HTTP request/response body that represents serialized parameters, * per the MIME type `application/x-www-form-urlencoded`. * * This class is immutable; all mutation operations return a new instance. * * @publicApi */ export class HttpParams { private map: Map|null; private encoder: HttpParameterCodec; private updates: Update[]|null = null; private cloneFrom: HttpParams|null = null; constructor(options: HttpParamsOptions = {} as HttpParamsOptions) { this.encoder = options.encoder || new HttpUrlEncodingCodec(); if (!!options.fromString) { if (!!options.fromObject) { throw new Error(`Cannot specify both fromString and fromObject.`); } this.map = paramParser(options.fromString, this.encoder); } else if (!!options.fromObject) { this.map = new Map(); Object.keys(options.fromObject).forEach(key => { const value = (options.fromObject as any)[key]; this.map !.set(key, Array.isArray(value) ? value : [value]); }); } else { this.map = null; } } /** * Reports whether the body includes one or more values for a given parameter. * @param param The parameter name. * @returns True if the parameter has one or more values, * false if it has no value or is not present. */ has(param: string): boolean { this.init(); return this.map !.has(param); } /** * Retrieves the first value for a parameter. * @param param The parameter name. * @returns The first value of the given parameter, * or `null` if the parameter is not present. */ get(param: string): string|null { this.init(); const res = this.map !.get(param); return !!res ? res[0] : null; } /** * Retrieves all values for a parameter. * @param param The parameter name. * @returns All values in a string array, * or `null` if the parameter not present. */ getAll(param: string): string[]|null { this.init(); return this.map !.get(param) || null; } /** * Retrieves all the parameters for this body. * @returns The parameter names in a string array. */ keys(): string[] { this.init(); return Array.from(this.map !.keys()); } /** * Appends a new value to existing values for a parameter. * @param param The parameter name. * @param value The new value to add. * @return A new body with the appended value. */ append(param: string, value: string): HttpParams { return this.clone({param, value, op: 'a'}); } /** * Replaces the value for a parameter. * @param param The parameter name. * @param value The new value. * @return A new body with the new value. */ set(param: string, value: string): HttpParams { return this.clone({param, value, op: 's'}); } /** * Removes a given value or all values from a parameter. * @param param The parameter name. * @param value The value to remove, if provided. * @return A new body with the given value removed, or with all values * removed if no value is specified. */ delete (param: string, value?: string): HttpParams { return this.clone({param, value, op: 'd'}); } /** * Serializes the body to an encoded string, where key-value pairs (separated by `=`) are * separated by `&`s. */ toString(): string { this.init(); return this.keys() .map(key => { const eKey = this.encoder.encodeKey(key); return this.map !.get(key) !.map(value => eKey + '=' + this.encoder.encodeValue(value)) .join('&'); }) .join('&'); } private clone(update: Update): HttpParams { const clone = new HttpParams({ encoder: this.encoder } as HttpParamsOptions); clone.cloneFrom = this.cloneFrom || this; clone.updates = (this.updates || []).concat([update]); return clone; } private init() { if (this.map === null) { this.map = new Map(); } if (this.cloneFrom !== null) { this.cloneFrom.init(); this.cloneFrom.keys().forEach(key => this.map !.set(key, this.cloneFrom !.map !.get(key) !)); this.updates !.forEach(update => { switch (update.op) { case 'a': case 's': const base = (update.op === 'a' ? this.map !.get(update.param) : undefined) || []; base.push(update.value !); this.map !.set(update.param, base); break; case 'd': if (update.value !== undefined) { let base = this.map !.get(update.param) || []; const idx = base.indexOf(update.value); if (idx !== -1) { base.splice(idx, 1); } if (base.length > 0) { this.map !.set(update.param, base); } else { this.map !.delete(update.param); } } else { this.map !.delete(update.param); break; } } }); this.cloneFrom = this.updates = null; } } }