forked from Damianonymous/MFCAuto
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathUtils.ts
361 lines (333 loc) · 13.6 KB
/
Utils.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
const load = require("load");
import { spawn } from "child_process";
import * as assert from "assert";
import * as fs from "fs";
import * as path from "path";
import * as request from "request-promise-native";
import * as cheerio from "cheerio";
export enum LogLevel {
/** No logging to the console, not even errors */
SILENT,
/** Only fatal or state corrupting errors */
ERROR,
/** Non-fatal warnings */
WARNING,
/** Status info, this is the default logging level */
INFO,
/** More verbose status info */
VERBOSE,
/** Debug information that won't be useful to most people */
DEBUG,
/** Debug information plus the entire packet log. This is very very verbose. */
TRACE,
}
let logLevel = LogLevel.INFO;
let defaultLogFileName: string | undefined;
let defaultConsoleFormatter: ((msg: string) => string) | null | undefined;
/**
* Sets default logging options
* @param level Maximum LogLevel for which to log
* @param logFileName Default file to log to
* @param consoleFormatter Default formatter, usually you should leave this alone except
* to possibly specify 'null' to turn off all console logging while leaving a fileRoot
* to log only to a file instead
*/
export function setLogLevel(level: LogLevel, logFileName?: string, consoleFormatter?: ((msg: string) => string) | null) {
"use strict";
logLevel = level;
defaultLogFileName = logFileName;
defaultConsoleFormatter = consoleFormatter;
}
export function logWithLevelInternal(level: LogLevel, msg: string | (() => string), logFileName?: string, consoleFormatter?: ((msg: string) => string) | null): void {
if (logFileName === undefined) {
logFileName = defaultLogFileName;
}
if (consoleFormatter === undefined) {
consoleFormatter = defaultConsoleFormatter;
}
logWithLevel(level, msg, logFileName, consoleFormatter);
}
export function logInternal(msg: string | (() => string), logFileName?: string, consoleFormatter?: ((msg: string) => string) | null): void {
if (logFileName === undefined) {
logFileName = defaultLogFileName;
}
if (consoleFormatter === undefined) {
consoleFormatter = defaultConsoleFormatter;
}
log(msg, logFileName, consoleFormatter);
}
// Like "log" but respects different levels
export function logWithLevel(level: LogLevel, msg: string | (() => string), logFileName?: string, consoleFormatter?: ((msg: string) => string) | null): void {
"use strict";
if (logLevel >= level) {
log(msg, logFileName, consoleFormatter);
}
}
// Pads single digit number with a leading zero, simple helper function
function toStr(n: number): string {
return n < 10 ? "0" + n.toString() : "" + n.toString();
}
function getDateTimeString(): string {
const d = new Date();
return (d.getFullYear().toString()) + "/" + (toStr(d.getMonth() + 1)) + "/" + (toStr(d.getDate())) + " - " + (toStr(d.getHours())) + ":" + (toStr(d.getMinutes())) + ":" + (toStr(d.getSeconds()));
}
// Helper logging function that timestamps each message and optionally outputs to a file as well
export function log(msg: string | (() => string), logFileName?: string, consoleFormatter?: ((msg: string) => string) | null): void {
"use strict";
assert.notStrictEqual(msg, undefined, "Trying to print undefined. This usually indicates a bug upstream from the log function.");
if (msg instanceof Function) {
msg = msg();
}
const taggedMsg = `[${getDateTimeString()}${(logFileName !== undefined ? `, ${logFileName.toUpperCase()}` : "")}] ${msg}`;
// Explicitly passing null, not undefined, as the consoleFormatter
// means to skip the console output completely
// tslint:disable-next-line:no-null-keyword
if (consoleFormatter !== null) {
if (consoleFormatter !== undefined) {
console.log(consoleFormatter(taggedMsg));
} else {
console.log(taggedMsg);
}
}
if (logFileName !== undefined) {
const fd = fs.openSync(logFileName, "a");
fs.writeSync(fd, taggedMsg + "\r\n");
fs.closeSync(fd);
}
}
// Takes a string, detects if it was URI encoded,
// and returns the decoded version
export function decodeIfNeeded(str: string): string {
if (typeof str === "string" && str.indexOf("%") !== -1) {
try {
const decoded = decodeURIComponent(str);
if (decoded === str) {
// Apparently it wasn't actually encoded
// So just return it
return str;
} else {
// If it was fully URI encoded, then re-encoding
// the decoded should return the original
const encoded = encodeURIComponent(decoded);
if (encoded === str) {
// Yep, it was fully encoded
return decoded;
} else {
// It wasn't fully encoded, maybe it wasn't
// encoded at all. Be safe and return the
// original
logWithLevel(LogLevel.DEBUG, () => `[UTILS] decodeIfNeeded detected partially encoded string? '${str}'`);
return str;
}
}
} catch (e) {
logWithLevel(LogLevel.DEBUG, () => `[UTILS] decodeIfNeeded exception decoding '${str}'`);
return str;
}
} else {
return str;
}
}
// tslint:disable-next-line:no-any
export function decodeAny(anything: any): any {
if (typeof anything === "string") {
anything = decodeIfNeeded(anything);
} else if (Array.isArray(anything)) {
anything.forEach((value, index) => {
// tslint:disable-next-line:no-unsafe-any
anything[index] = decodeAny(value);
});
} else if (typeof anything === "object" && anything !== null) {
Object.getOwnPropertyNames(anything).forEach((key) => {
// tslint:disable-next-line:no-unsafe-any
anything[key] = decodeAny(anything[key]);
});
}
return anything;
}
// Deprecated. This function is no longer used and may be removed from
// future versions of MFCAuto. For mixin patterns, please move to the
// new TypeScript 2.2+ syntax as described here:
// https://www.typescriptlang.org/docs/handbook/release-notes/typescript-2-2.html
export function applyMixins(derivedCtor: Function, baseCtors: Function[]) {
"use strict";
baseCtors.forEach(baseCtor => {
Object.getOwnPropertyNames(baseCtor.prototype).forEach(name => {
// tslint:disable-next-line:no-unsafe-any
derivedCtor.prototype[name] = baseCtor.prototype[name];
});
});
}
// tslint:disable-next-line:no-any
export type Constructor<T> = new (...args: any[]) => T;
/**
* Helper function to find the full path to an executable of a given name
* that should have been brought down as a direct npm dependency of MFCAuto.
*/
export function findDependentExe(name: string): string {
let location: string | undefined;
const firstPlaceToCheck = path.join(path.dirname(__dirname), "node_modules", ".bin", name);
if (fs.existsSync(firstPlaceToCheck)) {
location = firstPlaceToCheck;
} else {
let startingDir = __dirname;
let nextDirUp = path.dirname(__dirname);
while (nextDirUp !== startingDir) {
const placeToCheck = path.join(nextDirUp, ".bin", name);
if (fs.existsSync(placeToCheck)) {
location = placeToCheck;
break;
} else {
startingDir = nextDirUp;
nextDirUp = path.dirname(startingDir);
}
}
}
// Sanity check the results
if (location === undefined) {
logWithLevel(LogLevel.WARNING, `Could not find ${name} at the expected location. Assuming it is on the PATH and invoking anyway. If this fails, please re-run 'npm install'.`);
return name;
}
if (!fs.statSync(location).isFile()) {
throw new Error(`Found ${name}, but it is not a file?`);
}
try {
fs.accessSync(location, fs.constants.X_OK);
} catch (e) {
throw new Error(`Found ${name}, but the calling process does not have execute permissions`);
}
return location;
}
/**
* Helper function that spawns the given executable with the given arguments
* and returns a promise that resolves with all the text the process wrote
* to stdout, or rejects if the process couldn't be run or had any stderr output
*/
export async function spawnOutput(command: string, args?: string[]): Promise<string> {
// @TODO - Might be good to have a timeout in case the spawned process hangs
return new Promise<string>((resolve, reject) => {
const ps = spawn(command, args, { shell: true });
let stdout = "";
let stderr = "";
ps.on("error", (err) => {
reject(err);
});
ps.on("exit", (/*code, signal*/) => {
if (stderr.length > 0) {
reject(stderr);
} else {
resolve(stdout);
}
});
ps.stdout.on("data", (data) => {
stdout += data;
});
ps.stderr.on("data", (data) => {
stderr += data;
});
});
}
/**
* Takes a string representation of a JS object, with potentially
* unquoted or single quoted keys, converts it to a form that
* can be parsed with JSON.parse, and returns the parsed result.
* @param input
*/
export function parseJsObj(input: string) {
return JSON.parse(input.replace(/(['"])?([a-zA-Z0-9_\$]+)(['"])?\s*:/g, '"$2":').replace(/'/g, '"'));
}
// tslint:disable:no-any no-unsafe-any
/**
* Dynamically loads script code from the web, massaging it with the given
* massager function first, and then passes the resulting instantiated object
* to the given callback.
*
* We try to use this sparingly as it opens us up to breaks from site changes.
* But it is still useful for the more complex or frequently updated parts
* of MFC.
* @param url URL from which to load the site script
* @param massager Post-processor function that takes the raw site script and
* converts/massages it to a usable form.
* @returns A promise that resolves with the object loaded from site code
* @access private
*/
export async function loadFromWeb(url: string, massager?: (src: string) => string): Promise<any> {
let contents = await request(url).promise() as string;
if (massager !== undefined) {
contents = massager(contents);
}
return (load.compiler(contents));
}
/**
* Creates an object suitable for use as the POST payload for a given HTML form.
* This will not correctly handle any form elements that are dynamically added,
* removed, or altered by page script. Fortunately, MFC doesn't seem to do that
* at the moment.
* @param form A CheerioElement containing the HTML form to create parameters for
* @param userOptions Any user provided overrides for the page defaults
* @access private
*/
export function createFormInput<T, U>(form: CheerioElement, userOptions: T | undefined): U {
const parsedOptions: any = {};
const inputs = cheerio.load(form)("input");
const formValues: Array<{ name: string, value?: string, disabled?: string, type?: string, checked?: string }> = [];
inputs.each((_index, element) => {
const currentInput = {
name: element.attribs.name,
value: element.attribs.value,
disabled: element.attribs.disabled,
type: element.attribs.type,
checked: element.attribs.checked,
};
formValues.push(currentInput);
if (!currentInput.disabled) {
if (currentInput.type === "checkbox") {
if (currentInput.checked) {
parsedOptions[currentInput.name] = currentInput.value || "on";
}
} else {
if (currentInput.value !== undefined && currentInput.value !== "") {
parsedOptions[currentInput.name] = currentInput.value;
}
}
}
// @TODO - How should I handle a disabled input field
// for which we have a userOption specified? Should
// the userOption cause the field to be enabled?
// Can tackle this later though as, right now, I
// don't see any disabled input fields on Share.
});
// Merge in our options bag argument, but only take the
// properties that are valid for this form.
//
// In the future, we might remove or reverse this
// limitation and trust the given userOptions more
// than the parsedOptions. That could be one way
// to handle dynamically generated form elements.
// But for now this style of limitation is more useful
// because I want one single "SharePurchaseOptions" type
// and I know not every field on it will be valid for
// every share item type. So I want to ignore the ones
// that are invalid.
if (userOptions !== undefined) {
formValues.forEach((fv) => {
if (fv.name in userOptions) {
const anyOptions = userOptions as any;
if (fv.type === "checkbox") {
if (typeof (anyOptions[fv.name]) !== "boolean") {
throw new Error(`createFormInput error: '${fv.name}' option is a checkbox and should be specified as a boolean`);
}
if (anyOptions[fv.name]) {
parsedOptions[fv.name] = fv.value || "on";
} else {
delete parsedOptions[fv.name];
}
} else {
parsedOptions[fv.name] = anyOptions[fv.name];
}
}
});
}
return parsedOptions as U;
}
// tslint:enable:no-any no-unsafe-any