зеркало из
1
0
Форкнуть 0
opensource-management-portal/utils.ts

428 строки
13 KiB
TypeScript

//
// Copyright (c) Microsoft.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
//
import { Response, Request, Router } from 'express';
import fs from 'fs';
import path from 'path';
import { URL } from 'url';
import zlib from 'zlib';
import { ReposAppRequest, IAppSession, IReposError, SiteConfiguration } from './interfaces';
import { getProviders } from './transitional';
export function daysInMilliseconds(days: number): number {
return 1000 * 60 * 60 * 24 * days;
}
export function stringOrNumberAsString(value: any) {
if (typeof value === 'number') {
return (value as number).toString();
} else if (typeof value === 'string') {
return value;
}
const typeName = typeof value;
throw new Error(`Unsupported type ${typeName} for value ${value} (stringOrNumberAsString)`);
}
export function stringOrNumberArrayAsStringArray(values: any[]) {
return values.map((val) => stringOrNumberAsString(val));
}
export function requireJson(nameFromRoot: string): any {
// In some situations TypeScript can load from JSON, but for the transition this is better to reach outside the out directory
let file = path.resolve(__dirname, nameFromRoot);
// If within the output directory
if (fs.existsSync(file)) {
const content = fs.readFileSync(file, 'utf8');
return JSON.parse(content);
}
file = path.resolve(__dirname, '..', nameFromRoot);
if (!fs.existsSync(file)) {
throw new Error(`Cannot find JSON file ${file} to read as a module`);
}
const content = fs.readFileSync(file, 'utf8');
console.warn(`JSON as module (${file}) from project root (NOT TypeScript 'dist' folder)`);
return JSON.parse(content);
}
// ----------------------------------------------------------------------------
// Returns an integer, random, between low and high (exclusive) - [low, high)
// ----------------------------------------------------------------------------
export function randomInteger(low: number, high: number) {
return Math.floor(Math.random() * (high - low) + low);
}
export function safeLocalRedirectUrl(path: string) {
if (!path) {
return;
}
const url = new URL(path, 'http://localhost');
if (url.host !== 'localhost') {
return;
}
return url.search ? `${url.pathname}${url.search}` : url.pathname;
}
// Session utility: Store the referral URL, if present, and redirect to a new
// location.
interface IStoreReferrerEventDetails {
method: string;
reason: string;
referer?: string;
redirect?: string;
}
export function storeReferrer(req: ReposAppRequest, res, redirect, optionalReason) {
const { insights } = getProviders(req);
const eventDetails: IStoreReferrerEventDetails = {
method: 'storeReferrer',
reason: optionalReason || 'unknown reason',
};
const session = req.session as IAppSession;
if (
session &&
req.headers &&
req.headers.referer &&
session.referer !== undefined &&
!req.headers.referer.includes('/signout') &&
!session.referer
) {
session.referer = req.headers.referer;
eventDetails.referer = req.headers.referer;
} else {
eventDetails.referer = 'no referer';
}
if (redirect) {
eventDetails.redirect = redirect;
insights?.trackEvent({ name: 'RedirectWithReferrer', properties: eventDetails });
res.redirect(redirect);
}
}
export function sortByCaseInsensitive(a: string, b: string) {
let nameA = a.toLowerCase();
let nameB = b.toLowerCase();
if (nameA < nameB) {
return -1;
}
if (nameA > nameB) {
return 1;
}
return 0;
}
// ----------------------------------------------------------------------------
// Session utility: store the original URL
// ----------------------------------------------------------------------------
export function storeOriginalUrlAsReferrer(
req: Request,
res: Response,
redirect: string,
optionalReason?: string
) {
storeOriginalUrlAsVariable(req, res, 'referer', redirect, optionalReason);
}
export function redirectToReferrer(req, res, url, optionalReason) {
url = url || '/';
const alternateUrl = popSessionVariable(req, res, 'referer');
const eventDetails = {
method: 'redirectToReferrer',
reason: optionalReason || 'unknown reason',
};
if (req.insights) {
req.insights.trackEvent({ name: 'RedirectToReferrer', properties: eventDetails });
}
res.redirect(alternateUrl || url);
}
export function storeOriginalUrlAsVariable(req, res, variable, redirect, optionalReason) {
const eventDetails = {
method: 'storeOriginalUrlAsVariable',
variable,
redirect,
reason: optionalReason || 'unknown reason',
};
if (req.session && req.originalUrl) {
req.session[variable] = req.originalUrl;
eventDetails['ou'] = req.originalUrl;
}
if (redirect) {
if (req.insights) {
req.insights.trackEvent({ name: 'RedirectFromOriginalUrl', properties: eventDetails });
}
res.redirect(redirect);
}
}
export function popSessionVariable(req, res, variableName) {
if (req.session && req.session[variableName] !== undefined) {
const url = req.session[variableName];
delete req.session[variableName];
return url;
}
}
// ----------------------------------------------------------------------------
// Provide our own error wrapper and message for an underlying thrown error.
// Useful for the user-presentable version.
// ----------------------------------------------------------------------------
const errorPropertiesToClone = ['stack', 'status'];
export function wrapError(error, message, userIntendedMessage?: boolean): IReposError {
const err: IReposError = new Error(message);
err.innerError = error;
if (error) {
for (let i = 0; i < errorPropertiesToClone.length; i++) {
const key = errorPropertiesToClone[i];
const value = error[key];
if (value && typeof value === 'number') {
// Store as a string
err[key] = value.toString();
} else if (value) {
err[key] = value;
}
}
}
if (userIntendedMessage === true) {
err.skipLog = true;
}
return err;
}
// ----------------------------------------------------------------------------
// A destructive removal function for an object. Removes a single key.
// ----------------------------------------------------------------------------
export function stealValue(obj, key) {
if (obj[key] !== undefined) {
let val = obj[key];
delete obj[key];
return val;
}
}
// ----------------------------------------------------------------------------
// Given a list of string values, check a string, using a case-insensitive
// comparison.
// ----------------------------------------------------------------------------
export function inListInsensitive(list, value) {
value = value.toLowerCase();
for (let i = 0; i < list.length; i++) {
if (list[i].toLowerCase() === value) {
return true;
}
}
return false;
}
// ----------------------------------------------------------------------------
// Given a list of lowercase values, check whether a value is present.
// ----------------------------------------------------------------------------
export function isInListAnyCaseInLowerCaseList(list, value) {
value = value.toLowerCase();
for (let i = 0; i < list.length; i++) {
if (list[i] === value) {
return true;
}
}
return false;
}
// ----------------------------------------------------------------------------
// Given an array of things that have an `id` property, return a hash indexed
// by that ID.
// ----------------------------------------------------------------------------
export function arrayToHashById(inputArray) {
let hash = {};
if (inputArray && inputArray.length) {
for (let i = 0; i < inputArray.length; i++) {
if (inputArray[i] && inputArray[i].id) {
hash[inputArray[i].id] = inputArray[i];
}
}
}
return hash;
}
// ----------------------------------------------------------------------------
// Obfuscate a string value, optionally leaving a few characters visible.
// ----------------------------------------------------------------------------
export function obfuscate(value, lastCharactersShowCount) {
if (value === undefined || value === null || value.length === undefined) {
return value;
}
let length = value.length;
lastCharactersShowCount = lastCharactersShowCount || 0;
lastCharactersShowCount = Math.min(Math.round(lastCharactersShowCount), length - 1);
let obfuscated = '';
for (let i = 0; i < length - lastCharactersShowCount; i++) {
obfuscated += '*';
}
for (let j = length - lastCharactersShowCount; j < length; j++) {
obfuscated += value[j];
}
return obfuscated;
}
// ----------------------------------------------------------------------------
// A very basic breadcrumb stack that ties in to an Express request object.
// ----------------------------------------------------------------------------
export function addBreadcrumb(req, breadcrumbTitle, optionalBreadcrumbLink) {
if (req === undefined || req.baseUrl === undefined) {
throw new Error('addBreadcrumb: did you forget to provide a request object instance?');
}
if (!optionalBreadcrumbLink && optionalBreadcrumbLink !== false) {
optionalBreadcrumbLink = req.baseUrl;
}
if (!optionalBreadcrumbLink && optionalBreadcrumbLink !== false) {
optionalBreadcrumbLink = '/';
}
let breadcrumbs = req.breadcrumbs;
if (breadcrumbs === undefined) {
breadcrumbs = [];
}
breadcrumbs.push({
title: breadcrumbTitle,
url: optionalBreadcrumbLink,
});
req.breadcrumbs = breadcrumbs;
}
export function stackSafeCallback(callback, err, item, extraItem) {
// Works around RangeError: Maximum call stack size exceeded.
setImmediate(() => {
callback(err, item, extraItem);
});
}
export function createSafeCallbackNoParams(cb) {
return () => {
exports.stackSafeCallback(cb);
};
}
export function sleep(milliseconds: number): Promise<void> {
return new Promise((resolve, reject) => {
setTimeout(() => {
process.nextTick(resolve);
}, milliseconds);
});
}
export function ParseReleaseReviewWorkItemId(uri: string): string {
const safeUrl = new URL(uri);
const id = safeUrl.searchParams.get('id');
if (id) {
return id;
}
const pathname = safeUrl.pathname;
const editIndex = pathname.indexOf('edit/');
if (editIndex >= 0) {
return pathname.substr(editIndex + 5);
}
if (safeUrl.host === 'osstool.microsoft.com') {
return null; // Very legacy
}
throw new Error(`Unable to parse work item information from: ${uri}`);
}
export function readFileToText(filename: string): Promise<string> {
return new Promise((resolve, reject) => {
return fs.readFile(filename, 'utf8', (error, data) => {
return error ? reject(error) : resolve(data);
});
});
}
export function writeTextToFile(filename: string, stringContent: string): Promise<void> {
return new Promise((resolve, reject) => {
return fs.writeFile(filename, stringContent, 'utf8', (error) => {
if (error) {
console.warn(`Trouble writing ${filename} ${error}`);
} else {
console.log(`Wrote ${filename}`);
}
return error ? reject(error) : resolve();
});
});
}
export function quitInTenSeconds(successful: boolean) {
console.log(`Quitting process in 10s... exit code=${successful ? 0 : 1}`);
return setTimeout(() => {
process.exit(successful ? 0 : 1);
}, 1000 * 10 /* 10s */);
}
export function gzipString(value: string): Promise<Buffer> {
return new Promise((resolve, reject) => {
const val = Buffer.from(value);
zlib.gzip(val, (gzipError, compressed: Buffer) => {
return gzipError ? reject(gzipError) : resolve(compressed);
});
});
}
export function gunzipBuffer(buffer: Buffer): Promise<string> {
return new Promise((resolve, reject) => {
zlib.gunzip(buffer, (unzipError, unzipped) => {
// Fallback if there is a data error (i.e. it's not compressed)
if (unzipError && (unzipError as any)?.errno === zlib.Z_DATA_ERROR) {
const originalValue = buffer.toString();
return resolve(originalValue);
} else if (unzipError) {
return reject(unzipError);
}
try {
const unzippedValue = unzipped.toString();
return resolve(unzippedValue);
} catch (otherError) {
return reject(otherError);
}
});
});
}
export function swapMap(map: Map<string, string>): Map<string, string> {
const rm = new Map<string, string>();
for (const [key, value] of map.entries()) {
rm.set(value, key);
}
return rm;
}
export function addArrayToSet<T>(set: Set<T>, array: T[]): Set<T> {
for (const entry of array) {
set.add(entry);
}
return set;
}
export function isEnterpriseManagedUserLogin(login: string) {
return login?.includes('_');
}
export function isCodespacesAuthenticating(config: SiteConfiguration, authType: 'aad' | 'github') {
const { codespaces } = config?.github || {};
return (
codespaces?.connected === true &&
codespaces?.authentication &&
codespaces.authentication[authType] &&
codespaces.authentication[authType].enabled
);
}
export function getCodespacesHostname(config: SiteConfiguration) {
const { github, webServer } = config;
const { codespaces } = github;
const { connected, desktop } = codespaces;
let codespacesPort = undefined;
if (connected === true) {
codespacesPort = codespaces.authentication?.port;
}
const port = codespacesPort || webServer.port || 3000;
const forwardingDomain = codespaces?.forwardingDomain || 'preview.app.github.dev';
return desktop ? `http://localhost:${port}` : `https://${codespaces.name}-${port}.${forwardingDomain}`;
}