@alfresco/adf-core
Version:
517 lines (507 loc) • 20.6 kB
JavaScript
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