/*! * @license * Copyright 2019 Alfresco Software, Ltd. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ import { Emitters as JsApiEmitters, HttpClient as JsApiHttpClient, RequestOptions, SecurityOptions, isBrowser } from '@alfresco/js-api'; import { HttpClient, HttpErrorResponse, HttpEvent, HttpHeaders, HttpParams, HttpResponse } from '@angular/common/http'; import { Injectable } from '@angular/core'; import { Observable, of, Subject, throwError } from 'rxjs'; import { catchError, map, takeUntil } from 'rxjs/operators'; import { convertObjectToFormData, getQueryParamsWithCustomEncoder, isBlobResponse, isConstructor, isHttpResponseEvent, isHttpUploadProgressEvent, removeNilValues } from './alfresco-api.utils'; import { AlfrescoApiParamEncoder } from './alfresco-api.param-encoder'; import { AlfrescoApiResponseError } from './alfresco-api.response-error'; import { Constructor } from '../types'; @Injectable({ providedIn: 'root' }) export class AlfrescoApiHttpClient implements JsApiHttpClient { constructor(private httpClient: HttpClient) {} request(url: string, options: RequestOptions, sc: SecurityOptions, emitters: JsApiEmitters): Promise { const body = AlfrescoApiHttpClient.getBody(options); const params = getQueryParamsWithCustomEncoder(options.queryParams, new AlfrescoApiParamEncoder()); const headers = AlfrescoApiHttpClient.getHeaders(options); const responseType = AlfrescoApiHttpClient.getResponseType(options); const request = this.httpClient.request( options.httpMethod, url, { ...(body && { body }), ...(responseType && { responseType }), ...(sc.withCredentials && { withCredentials: true }), ...(params && { params }), headers, observe: 'events', reportProgress: true } ); return this.requestWithLegacyEventEmitters(request, emitters, options.returnType); } post(url: string, options: RequestOptions, sc: SecurityOptions, emitters: JsApiEmitters): Promise { return this.request(url, { ...options, httpMethod: 'POST' }, sc, emitters); } put(url: string, options: RequestOptions, sc: SecurityOptions, emitters: JsApiEmitters): Promise { return this.request(url, { ...options, httpMethod: 'PUT' }, sc, emitters); } get(url: string, options: RequestOptions, sc: SecurityOptions, emitters: JsApiEmitters): Promise { return this.request(url, { ...options, httpMethod: 'GET' }, sc, emitters); } delete(url: string, options: RequestOptions, sc: SecurityOptions, emitters: JsApiEmitters): Promise { return this.request(url, { ...options, httpMethod: 'DELETE' }, sc, emitters); } private requestWithLegacyEventEmitters(request$: Observable>, emitters: JsApiEmitters, returnType: any): Promise { const abort$ = new Subject(); const { eventEmitter, apiClientEmitter } = emitters; const promise = request$.pipe( map((res) => { if (isHttpUploadProgressEvent(res)) { const percent = Math.round((res.loaded / res.total) * 100); eventEmitter.emit('progress', { loaded: res.loaded, total: res.total, percent }); } if (isHttpResponseEvent(res)) { eventEmitter.emit('success', res.body); return AlfrescoApiHttpClient.deserialize(res, returnType); } }), catchError((err: HttpErrorResponse): Observable => { // since we can't always determinate ahead of time if the response is going to be xml or plain text response // we need to handle false positive cases here. if (err.status === 200) { eventEmitter.emit('success', err.error.text); return of(err.error.text); } eventEmitter.emit('error', err); apiClientEmitter.emit('error', err); if (err.status === 401) { eventEmitter.emit('unauthorized'); apiClientEmitter.emit('unauthorized'); } // for backwards compatibility we need to convert it to error class as the HttpErrorResponse only implements Error interface, not extending it, // and we need to be able to correctly pass instanceof Error conditions used inside repository // we also need to pass error as Stringify string as we are detecting statusCodes using JSON.parse(error.message) in some places const msg = typeof err.error === 'string' ? err.error : JSON.stringify(err.error); // for backwards compatibility to handle cases in code where we try read response.error.response.body; const error = { response: { ...err, body: err.error } }; const alfrescoApiError = new AlfrescoApiResponseError(msg, err.status, error); return throwError(alfrescoApiError); }), takeUntil(abort$) ).toPromise(); (promise as any).abort = function() { eventEmitter.emit('abort'); abort$.next(); abort$.complete(); return this; }; return promise; } private static getBody(options: RequestOptions): any { const contentType = options.contentType; const isFormData = contentType === 'multipart/form-data'; const isFormUrlEncoded = contentType === 'application/x-www-form-urlencoded'; const body = options.bodyParam; if (isFormData) { return convertObjectToFormData(options.formParams); } if (isFormUrlEncoded) { return new HttpParams({ fromObject: removeNilValues(options.formParams) }); } return body; } private static getHeaders(options: RequestOptions): HttpHeaders { const optionsHeaders = { ...options.headerParams, ...(options.accept && { Accept: options.accept }), ...((options.contentType) && { 'Content-Type': options.contentType }) }; return new HttpHeaders(optionsHeaders); } private static getResponseType(options: RequestOptions): 'blob' | 'json' | 'text' { const isBlobType = options.returnType?.toString().toLowerCase() === 'blob' || options.responseType?.toString().toLowerCase() === 'blob'; if (isBlobType) { return 'blob'; } if (options.returnType === 'String') { return 'text'; } return 'json'; } /** * Deserialize an HTTP response body into a value of the specified type. */ private static deserialize(response: HttpResponse, returnType?: Constructor | 'blob'): any { if (response === null) { return null; } const body = response.body; if (!returnType) { // for backwards compatibility we need to return empty string instead of null, // to avoid issues when accessing null response would break application [C309878] // cannot read property 'entry' of null in cases like // return this.post(apiUrl, saveFormRepresentation).pipe(map((res: any) => res.entry)) return body !== null ? body : ''; } if (isBlobResponse(response, returnType)) { return AlfrescoApiHttpClient.deserializeBlobResponse(response); } if (!isConstructor(returnType)) { return body; } if (Array.isArray(body)) { return body.map((element) => new returnType(element)); } return new returnType(body); } private static deserializeBlobResponse(response: HttpResponse) { if (isBrowser()) { return new Blob([response.body], { type: response.headers.get('Content-Type') }); } return Buffer.from(response.body as unknown as WithImplicitCoercion, 'binary'); } }