1
0
mirror of https://github.com/bitwarden/browser.git synced 2024-12-20 16:07:45 +01:00
bitwarden-browser/apps/cli/src/utils.ts
Matt Gibson 9c1e2ebd67
Typescript-strict-plugin (#12235)
* Use typescript-strict-plugin to iteratively turn on strict

* Add strict testing to pipeline

Can be executed locally through either `npm run test:types` for full type checking including spec files, or `npx tsc-strict` for only tsconfig.json included files.

* turn on strict for scripts directory

* Use plugin for all tsconfigs in monorepo

vscode is capable of executing tsc with plugins, but uses the most relevant tsconfig to do so. If the plugin is not a part of that config, it is skipped and developers get no feedback of strict compile time issues. These updates remedy that at the cost of slightly more complex removal of the plugin when the time comes.

* remove plugin from configs that extend one that already has it

* Update workspace settings to honor strict plugin

* Apply strict-plugin to native message test runner

* Update vscode workspace to use root tsc version

* `./node_modules/.bin/update-strict-comments` 🤖

This is a one-time operation. All future files should adhere to strict type checking.

* Add fixme to `ts-strict-ignore` comments

* `update-strict-comments` 🤖

repeated for new merge files
2024-12-09 20:58:50 +01:00

275 lines
8.6 KiB
TypeScript

// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
import * as fs from "fs";
import * as path from "path";
import * as inquirer from "inquirer";
import * as JSZip from "jszip";
import { CollectionView } from "@bitwarden/admin-console/common";
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
import { Utils } from "@bitwarden/common/platform/misc/utils";
import { FolderView } from "@bitwarden/common/vault/models/view/folder.view";
import { NodeUtils } from "@bitwarden/node/node-utils";
import { Response } from "./models/response";
import { MessageResponse } from "./models/response/message.response";
export class CliUtils {
static writeLn(s: string, finalLine = false, error = false) {
const stream = error ? process.stderr : process.stdout;
if (finalLine && (process.platform === "win32" || !stream.isTTY)) {
stream.write(s);
} else {
stream.write(s + "\n");
}
}
static readFile(input: string): Promise<string> {
return new Promise<string>((resolve, reject) => {
let p: string = null;
if (input != null && input !== "") {
const osInput = path.join(input);
if (osInput.indexOf(path.sep) === -1) {
p = path.join(process.cwd(), osInput);
} else {
p = osInput;
}
} else {
reject("You must specify a file path.");
}
fs.readFile(p, "utf8", (err, data) => {
if (err != null) {
reject(err.message);
}
resolve(data);
});
});
}
static extractZipContent(input: string, filepath: string): Promise<string> {
return new Promise<string>((resolve, reject) => {
let p: string = null;
if (input != null && input !== "") {
const osInput = path.join(input);
if (osInput.indexOf(path.sep) === -1) {
p = path.join(process.cwd(), osInput);
} else {
p = osInput;
}
} else {
reject("You must specify a file path.");
}
fs.readFile(p, function (err, data) {
if (err) {
reject(err);
}
JSZip.loadAsync(data).then(
(zip) => {
resolve(zip.file(filepath).async("string"));
},
(reason) => {
reject(reason);
},
);
});
});
}
/**
* Save the given data to a file and determine the target file if necessary.
* If output is non-empty, it is used as target filename. Otherwise the target filename is
* built from the current working directory and the given defaultFileName.
*
* @param data to be written to the file.
* @param output file to write to or empty to choose automatically.
* @param defaultFileName to use when no explicit output filename is given.
* @return the chosen output file.
*/
static saveFile(data: string | Buffer, output: string, defaultFileName: string) {
let p: string = null;
let mkdir = false;
if (output != null && output !== "") {
const osOutput = path.join(output);
if (osOutput.indexOf(path.sep) === -1) {
p = path.join(process.cwd(), osOutput);
} else {
mkdir = true;
if (osOutput.endsWith(path.sep)) {
p = path.join(osOutput, defaultFileName);
} else {
p = osOutput;
}
}
} else {
p = path.join(process.cwd(), defaultFileName);
}
p = path.resolve(p);
if (mkdir) {
const dir = p.substring(0, p.lastIndexOf(path.sep));
if (!fs.existsSync(dir)) {
NodeUtils.mkdirpSync(dir, "700");
}
}
return new Promise<string>((resolve, reject) => {
fs.writeFile(p, data, { encoding: "utf8", mode: 0o600 }, (err) => {
if (err != null) {
reject("Cannot save file to " + p);
}
resolve(p);
});
});
}
/**
* Process the given data and write it to a file if possible. If the user requested RAW output and
* no output name is given, the file is directly written to stdout. The resulting Response contains
* an otherwise empty message then to prevent writing other information to stdout.
*
* If an output is given or no RAW output is requested, the rules from [saveFile] apply.
*
* @param data to be written to the file or stdout.
* @param output file to write to or empty to choose automatically.
* @param defaultFileName to use when no explicit output filename is given.
* @return an empty [Response] if written to stdout or a [Response] with the chosen output file otherwise.
*/
static async saveResultToFile(data: string | Buffer, output: string, defaultFileName: string) {
if ((output == null || output === "") && process.env.BW_RAW === "true") {
// No output is given and the user expects raw output. Since the command result is about content,
// we directly return the command result to stdout (and suppress further messages).
process.stdout.write(data);
return Response.success();
}
const filePath = await this.saveFile(data, output, defaultFileName);
const res = new MessageResponse("Saved " + filePath, null);
res.raw = filePath;
return Response.success(res);
}
static readStdin(): Promise<string> {
return new Promise((resolve, reject) => {
let input = "";
if (process.stdin.isTTY) {
resolve(input);
return;
}
process.stdin.setEncoding("utf8");
process.stdin.on("readable", () => {
// eslint-disable-next-line
while (true) {
const chunk = process.stdin.read();
if (chunk == null) {
break;
}
input += chunk;
}
});
process.stdin.on("end", () => {
resolve(input);
});
});
}
static searchFolders(folders: FolderView[], search: string) {
search = search.toLowerCase();
return folders.filter((f) => {
if (f.name != null && f.name.toLowerCase().indexOf(search) > -1) {
return true;
}
return false;
});
}
static searchCollections(collections: CollectionView[], search: string) {
search = search.toLowerCase();
return collections.filter((c) => {
if (c.name != null && c.name.toLowerCase().indexOf(search) > -1) {
return true;
}
return false;
});
}
static searchOrganizations(organizations: Organization[], search: string) {
search = search.toLowerCase();
return organizations.filter((o) => {
if (o.name != null && o.name.toLowerCase().indexOf(search) > -1) {
return true;
}
return false;
});
}
/**
* Gets a password from all available sources. In order of priority these are:
* * passwordfile
* * passwordenv
* * user interaction
*
* Returns password string if successful, Response if not.
*/
static async getPassword(
password: string,
options: { passwordFile?: string; passwordEnv?: string },
logService?: LogService,
): Promise<string | Response> {
if (Utils.isNullOrEmpty(password)) {
if (options?.passwordFile) {
password = await NodeUtils.readFirstLine(options.passwordFile);
} else if (options?.passwordEnv) {
if (process.env[options.passwordEnv]) {
password = process.env[options.passwordEnv];
} else if (logService) {
logService.warning(`Warning: Provided passwordenv ${options.passwordEnv} is not set`);
}
}
}
if (Utils.isNullOrEmpty(password)) {
if (process.env.BW_NOINTERACTION !== "true") {
const answer: inquirer.Answers = await inquirer.createPromptModule({
output: process.stderr,
})({
type: "password",
name: "password",
message: "Master password:",
});
password = answer.password;
} else {
return Response.badRequest(
"Master password is required. Try again in interactive mode or provide a password file or environment variable.",
);
}
}
return password;
}
static convertBooleanOption(optionValue: any) {
return optionValue || optionValue === "" ? true : false;
}
static convertNumberOption(optionValue: any, defaultValue: number) {
try {
if (optionValue != null) {
const numVal = parseInt(optionValue);
return !Number.isNaN(numVal) ? numVal : defaultValue;
}
return defaultValue;
} catch {
return defaultValue;
}
}
static convertStringOption(optionValue: any, defaultValue: string) {
return optionValue != null ? String(optionValue) : defaultValue;
}
}