UNPKG

@alfresco/adf-core

Version:
517 lines (507 loc) 20.6 kB
import { SHOULD_ADD_AUTH_TOKEN } from '@alfresco/adf-core/auth'; import * as i1 from '@angular/common/http'; import { HttpEventType, HttpUrlEncodingCodec, HttpParams, HttpContext, HttpHeaders } from '@angular/common/http'; import * as i0 from '@angular/core'; import { Injectable } from '@angular/core'; import { Subject, lastValueFrom, of, throwError } from 'rxjs'; import { map, catchError, takeUntil } from 'rxjs/operators'; import { EventEmitter } from 'eventemitter3'; /*! * @license * Copyright © 2005-2025 Hyland Software, Inc. and its affiliates. All rights reserved. * * 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. */ /*! * @license * Copyright © 2005-2025 Hyland Software, Inc. and its affiliates. All rights reserved. * * 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. */ const isHttpUploadProgressEvent = (val) => val.type === HttpEventType.UploadProgress; const isHttpResponseEvent = (val) => val.type === HttpEventType.Response; const isDate = (value) => value instanceof Date; const isXML = (value) => typeof value === 'string' && value.startsWith('<?xml'); const isBlobResponse = (response, returnType) => returnType === 'blob' || response.body instanceof Blob; const isConstructor = (value) => typeof value === 'function' && !!value?.prototype?.constructor.name; const convertParamsToString = (value) => (isDate(value) ? value.toISOString() : value); const getQueryParamsWithCustomEncoder = (obj, encoder = new HttpUrlEncodingCodec()) => { if (!obj) { return undefined; } let httpParams = new HttpParams({ encoder }); const params = removeNilValues(obj); for (const key in params) { if (Object.prototype.hasOwnProperty.call(params, key)) { const value = params[key]; if (value instanceof Array) { const array = value.map(convertParamsToString).filter(Boolean); httpParams = httpParams.appendAll({ [key]: array }); } else { httpParams = httpParams.append(key, convertParamsToString(value)); } } } return httpParams; }; /** * Removes null and undefined values from an object. * * @param obj object to process * @returns object with updated values */ const removeNilValues = (obj) => { if (!obj) { return {}; } return Object.keys(obj).reduce((acc, key) => { const value = obj[key]; const isNil = value === undefined || value === null; return isNil ? acc : { ...acc, [key]: value }; }, {}); }; const convertObjectToFormData = (formParams) => { const formData = new FormData(); for (const key in formParams) { if (Object.prototype.hasOwnProperty.call(formParams, key)) { const value = formParams[key]; if (value instanceof File) { formData.append(key, value, value.name); } else if (Array.isArray(value)) { value.forEach((item) => formData.append(key, item)); } else { formData.append(key, value); } } } return formData; }; /*! * @license * Copyright © 2005-2025 Hyland Software, Inc. and its affiliates. All rights reserved. * * 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. */ // The default implementation of HttpParameterCodec from angular // does not encode some special characters like + with is causing issues with the alfresco js API and returns 500 error class AlfrescoApiParamEncoder { encodeKey(key) { return encodeURIComponent(key); } encodeValue(value) { return encodeURIComponent(value); } decodeKey(key) { return decodeURIComponent(key); } decodeValue(value) { return decodeURIComponent(value); } } /*! * @license * Copyright © 2005-2025 Hyland Software, Inc. and its affiliates. All rights reserved. * * 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. */ class AlfrescoApiResponseError extends Error { constructor(msg, status, response) { super(msg); this.status = status; this.response = response; this.name = 'AlfrescoApiResponseError'; } } /*! * @license * Copyright © 2005-2025 Hyland Software, Inc. and its affiliates. All rights reserved. * * 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. */ class AdfHttpClient { get disableCsrf() { return this._disableCsrf; } set disableCsrf(disableCsrf) { this._disableCsrf = disableCsrf; } constructor(httpClient) { this.httpClient = httpClient; this.eventEmitter = new EventEmitter(); this.defaultSecurityOptions = { withCredentials: true, isBpmRequest: false, authentications: {}, defaultHeaders: {} }; // No need for ee(this) anymore - we use composition instead of inheritance } // EventEmitter delegation methods on(event, fn, context) { this.eventEmitter.on(event, fn, context); return this; } off(event, fn, context) { this.eventEmitter.off(event, fn, context); return this; } once(event, fn, context) { this.eventEmitter.once(event, fn, context); return this; } emit(event, ...args) { return this.eventEmitter.emit(event, ...args); } setDefaultSecurityOption(options) { this.defaultSecurityOptions = this.merge(this.defaultSecurityOptions, options); } merge(...objects) { const result = {}; objects.forEach((source) => { Object.keys(source).forEach((prop) => { if (prop in result && Array.isArray(result[prop])) { result[prop] = result[prop].concat(source[prop]); } else if (prop in result && typeof result[prop] === 'object') { result[prop] = this.merge(result[prop], source[prop]); } else { result[prop] = source[prop]; } }); }); return result; } request(url, options, sc = this.defaultSecurityOptions, emitters) { const body = AdfHttpClient.getBody(options); const params = getQueryParamsWithCustomEncoder(options.queryParams, new AlfrescoApiParamEncoder()); const responseType = AdfHttpClient.getResponseType(options); const context = new HttpContext().set(SHOULD_ADD_AUTH_TOKEN, true); const security = { ...this.defaultSecurityOptions, ...sc }; const headers = this.getHeaders(options); if (!emitters) { emitters = this.getEventEmitters(); } const request = this.httpClient.request(options.httpMethod, url, { context, ...(body && { body }), ...(responseType && { responseType }), ...security, ...(params && { params }), headers, observe: 'events', reportProgress: true }); return this.requestWithLegacyEventEmitters(request, emitters, options.returnType); } post(url, options, sc, emitters) { return this.request(url, { ...options, httpMethod: 'POST' }, sc, emitters); } put(url, options, sc, emitters) { return this.request(url, { ...options, httpMethod: 'PUT' }, sc, emitters); } get(url, options, sc, emitters) { return this.request(url, { ...options, httpMethod: 'GET' }, sc, emitters); } delete(url, options, sc, emitters) { return this.request(url, { ...options, httpMethod: 'DELETE' }, sc, emitters); } addPromiseListeners(promise, eventEmitter) { const eventPromise = Object.assign(promise, { on(event, fn, context) { eventEmitter.on(event, fn, context); return this; }, once(event, fn, context) { eventEmitter.once(event, fn, context); return this; }, emit(event, ...args) { return eventEmitter.emit(event, ...args); }, off(event, fn, context) { eventEmitter.off(event, fn, context); return this; } }); return eventPromise; } getEventEmitters() { const apiClientEmitter = new EventEmitter(); // Bind this instance's methods to the apiClientEmitter for backward compatibility apiClientEmitter.on = this.on.bind(this); apiClientEmitter.off = this.off.bind(this); apiClientEmitter.once = this.once.bind(this); apiClientEmitter.emit = this.emit.bind(this); return { apiClientEmitter, eventEmitter: new EventEmitter() }; } requestWithLegacyEventEmitters(request$, emitters, returnType) { const abort$ = new Subject(); const { eventEmitter, apiClientEmitter } = emitters; const promise = lastValueFrom(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 AdfHttpClient.deserialize(res, returnType); } }), catchError((err) => { // 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, response: { req: 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 = { ...err, body: err.error }; const alfrescoApiError = new AlfrescoApiResponseError(msg, err.status, error); return throwError(alfrescoApiError); }), takeUntil(abort$))); promise.abort = function () { eventEmitter.emit('abort'); abort$.next(); abort$.complete(); return this; }; return this.addPromiseListeners(promise, eventEmitter); } static getBody(options) { const contentType = options.contentType ? options.contentType : AdfHttpClient.jsonPreferredMime(options.contentTypes); 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; } getHeaders(options) { const contentType = options.contentType || AdfHttpClient.jsonPreferredMime(options.contentTypes); const accept = options.accept || AdfHttpClient.jsonPreferredMime(options.accepts); const optionsHeaders = { ...options.headerParams, ...(accept && { Accept: accept }), ...(contentType && { 'Content-Type': contentType }) }; if (!this.disableCsrf) { this.setCsrfToken(optionsHeaders); } return new HttpHeaders(optionsHeaders); } /** * Chooses a content type from the given array, with JSON preferred; i.e. return JSON if included, otherwise return the first. * * @param contentTypes a contentType array * @returns The chosen content type, preferring JSON. */ static jsonPreferredMime(contentTypes) { if (!contentTypes?.length) { return 'application/json'; } for (let i = 0; i < contentTypes.length; i++) { if (AdfHttpClient.isJsonMime(contentTypes[i])) { return contentTypes[i]; } } return contentTypes[0]; } /** * Checks whether the given content type represents JSON.<br> * JSON content type examples:<br> * <ul> * <li>application/json</li> * <li>application/json; charset=UTF8</li> * <li>APPLICATION/JSON</li> * </ul> * * @param contentType The MIME content type to check. * @returns <code>true</code> if <code>contentType</code> represents JSON, otherwise <code>false</code>. */ static isJsonMime(contentType) { return Boolean(contentType?.match(/^application\/json(;.*)?$/i)); } setCsrfToken(optionsHeaders) { const token = this.createCSRFToken(); optionsHeaders['X-CSRF-TOKEN'] = token; try { document.cookie = 'CSRF-TOKEN=' + token + ';path=/'; } catch { /* continue regardless of error */ } } createCSRFToken(a) { const randomValue = AdfHttpClient.getSecureRandomValue(); return a ? (a ^ ((randomValue * 16) >> (a / 4))).toString(16) : ([1e16] + (1e16).toString()).replace(/[01]/g, this.createCSRFToken); } static getSecureRandomValue() { const max = Math.pow(2, 32); return window.crypto.getRandomValues(new Uint32Array(1))[0] / max; } static getResponseType(options) { 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. * * @param response response object * @param returnType return type * @returns deserialized object */ static deserialize(response, returnType) { 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 AdfHttpClient.deserializeBlobResponse(response); } if (!isConstructor(returnType)) { return body; } if (Array.isArray(body)) { return body.map((element) => new returnType(element)); } return new returnType(body); } static deserializeBlobResponse(response) { return new Blob([response.body], { type: response.headers.get('Content-Type') }); } static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.6", ngImport: i0, type: AdfHttpClient, deps: [{ token: i1.HttpClient }], target: i0.ɵɵFactoryTarget.Injectable }); } static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "19.2.6", ngImport: i0, type: AdfHttpClient, providedIn: 'root' }); } } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.6", ngImport: i0, type: AdfHttpClient, decorators: [{ type: Injectable, args: [{ providedIn: 'root' }] }], ctorParameters: () => [{ type: i1.HttpClient }] }); /*! * @license * Copyright © 2005-2025 Hyland Software, Inc. and its affiliates. All rights reserved. * * 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. */ /*! * @license * Copyright © 2005-2025 Hyland Software, Inc. and its affiliates. All rights reserved. * * 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. */ /** * Generated bundle index. Do not edit. */ export { AdfHttpClient }; //# sourceMappingURL=alfresco-adf-core-api.mjs.map