Move to eslint
[oweals/peertube.git] / server / helpers / core-utils.ts
1 /* eslint-disable no-useless-call */
2
3 /*
4   Different from 'utils' because we don't not import other PeerTube modules.
5   Useful to avoid circular dependencies.
6 */
7
8 import { createHash, HexBase64Latin1Encoding, randomBytes } from 'crypto'
9 import { basename, isAbsolute, join, resolve } from 'path'
10 import * as pem from 'pem'
11 import { URL } from 'url'
12 import { truncate } from 'lodash'
13 import { exec, ExecOptions } from 'child_process'
14
15 const objectConverter = (oldObject: any, keyConverter: (e: string) => string, valueConverter: (e: any) => any) => {
16   if (!oldObject || typeof oldObject !== 'object') {
17     return valueConverter(oldObject)
18   }
19
20   if (Array.isArray(oldObject)) {
21     return oldObject.map(e => objectConverter(e, keyConverter, valueConverter))
22   }
23
24   const newObject = {}
25   Object.keys(oldObject).forEach(oldKey => {
26     const newKey = keyConverter(oldKey)
27     newObject[newKey] = objectConverter(oldObject[oldKey], keyConverter, valueConverter)
28   })
29
30   return newObject
31 }
32
33 const timeTable = {
34   ms: 1,
35   second: 1000,
36   minute: 60000,
37   hour: 3600000,
38   day: 3600000 * 24,
39   week: 3600000 * 24 * 7,
40   month: 3600000 * 24 * 30
41 }
42
43 export function parseDurationToMs (duration: number | string): number {
44   if (typeof duration === 'number') return duration
45
46   if (typeof duration === 'string') {
47     const split = duration.match(/^([\d.,]+)\s?(\w+)$/)
48
49     if (split.length === 3) {
50       const len = parseFloat(split[1])
51       let unit = split[2].replace(/s$/i, '').toLowerCase()
52       if (unit === 'm') {
53         unit = 'ms'
54       }
55
56       return (len || 1) * (timeTable[unit] || 0)
57     }
58   }
59
60   throw new Error(`Duration ${duration} could not be properly parsed`)
61 }
62
63 export function parseBytes (value: string | number): number {
64   if (typeof value === 'number') return value
65
66   const tgm = /^(\d+)\s*TB\s*(\d+)\s*GB\s*(\d+)\s*MB$/
67   const tg = /^(\d+)\s*TB\s*(\d+)\s*GB$/
68   const tm = /^(\d+)\s*TB\s*(\d+)\s*MB$/
69   const gm = /^(\d+)\s*GB\s*(\d+)\s*MB$/
70   const t = /^(\d+)\s*TB$/
71   const g = /^(\d+)\s*GB$/
72   const m = /^(\d+)\s*MB$/
73   const b = /^(\d+)\s*B$/
74   let match
75
76   if (value.match(tgm)) {
77     match = value.match(tgm)
78     return parseInt(match[1], 10) * 1024 * 1024 * 1024 * 1024 +
79       parseInt(match[2], 10) * 1024 * 1024 * 1024 +
80       parseInt(match[3], 10) * 1024 * 1024
81   } else if (value.match(tg)) {
82     match = value.match(tg)
83     return parseInt(match[1], 10) * 1024 * 1024 * 1024 * 1024 +
84       parseInt(match[2], 10) * 1024 * 1024 * 1024
85   } else if (value.match(tm)) {
86     match = value.match(tm)
87     return parseInt(match[1], 10) * 1024 * 1024 * 1024 * 1024 +
88       parseInt(match[2], 10) * 1024 * 1024
89   } else if (value.match(gm)) {
90     match = value.match(gm)
91     return parseInt(match[1], 10) * 1024 * 1024 * 1024 +
92       parseInt(match[2], 10) * 1024 * 1024
93   } else if (value.match(t)) {
94     match = value.match(t)
95     return parseInt(match[1], 10) * 1024 * 1024 * 1024 * 1024
96   } else if (value.match(g)) {
97     match = value.match(g)
98     return parseInt(match[1], 10) * 1024 * 1024 * 1024
99   } else if (value.match(m)) {
100     match = value.match(m)
101     return parseInt(match[1], 10) * 1024 * 1024
102   } else if (value.match(b)) {
103     match = value.match(b)
104     return parseInt(match[1], 10) * 1024
105   } else {
106     return parseInt(value, 10)
107   }
108 }
109
110 function sanitizeUrl (url: string) {
111   const urlObject = new URL(url)
112
113   if (urlObject.protocol === 'https:' && urlObject.port === '443') {
114     urlObject.port = ''
115   } else if (urlObject.protocol === 'http:' && urlObject.port === '80') {
116     urlObject.port = ''
117   }
118
119   return urlObject.href.replace(/\/$/, '')
120 }
121
122 // Don't import remote scheme from constants because we are in core utils
123 function sanitizeHost (host: string, remoteScheme: string) {
124   const toRemove = remoteScheme === 'https' ? 443 : 80
125
126   return host.replace(new RegExp(`:${toRemove}$`), '')
127 }
128
129 function isTestInstance () {
130   return process.env.NODE_ENV === 'test'
131 }
132
133 function isProdInstance () {
134   return process.env.NODE_ENV === 'production'
135 }
136
137 function getAppNumber () {
138   return process.env.NODE_APP_INSTANCE
139 }
140
141 let rootPath: string
142
143 function root () {
144   if (rootPath) return rootPath
145
146   // We are in /helpers/utils.js
147   rootPath = join(__dirname, '..', '..')
148
149   if (basename(rootPath) === 'dist') rootPath = resolve(rootPath, '..')
150
151   return rootPath
152 }
153
154 // Thanks: https://stackoverflow.com/a/12034334
155 function escapeHTML (stringParam) {
156   if (!stringParam) return ''
157
158   const entityMap = {
159     '&': '&',
160     '<': '&lt;',
161     '>': '&gt;',
162     '"': '&quot;',
163     '\'': '&#39;',
164     '/': '&#x2F;',
165     '`': '&#x60;',
166     '=': '&#x3D;'
167   }
168
169   return String(stringParam).replace(/[&<>"'`=/]/g, s => entityMap[s])
170 }
171
172 function pageToStartAndCount (page: number, itemsPerPage: number) {
173   const start = (page - 1) * itemsPerPage
174
175   return { start, count: itemsPerPage }
176 }
177
178 function buildPath (path: string) {
179   if (isAbsolute(path)) return path
180
181   return join(root(), path)
182 }
183
184 // Consistent with .length, lodash truncate function is not
185 function peertubeTruncate (str: string, options: { length: number, separator?: RegExp, omission?: string }) {
186   const truncatedStr = truncate(str, options)
187
188   // The truncated string is okay, we can return it
189   if (truncatedStr.length <= options.length) return truncatedStr
190
191   // Lodash takes into account all UTF characters, whereas String.prototype.length does not: some characters have a length of 2
192   // We always use the .length so we need to truncate more if needed
193   options.length -= truncatedStr.length - options.length
194   return truncate(str, options)
195 }
196
197 function sha256 (str: string | Buffer, encoding: HexBase64Latin1Encoding = 'hex') {
198   return createHash('sha256').update(str).digest(encoding)
199 }
200
201 function sha1 (str: string | Buffer, encoding: HexBase64Latin1Encoding = 'hex') {
202   return createHash('sha1').update(str).digest(encoding)
203 }
204
205 function execShell (command: string, options?: ExecOptions) {
206   return new Promise<{ err?: Error, stdout: string, stderr: string }>((res, rej) => {
207     exec(command, options, (err, stdout, stderr) => {
208       // eslint-disable-next-line prefer-promise-reject-errors
209       if (err) return rej({ err, stdout, stderr })
210
211       return res({ stdout, stderr })
212     })
213   })
214 }
215
216 function promisify0<A> (func: (cb: (err: any, result: A) => void) => void): () => Promise<A> {
217   return function promisified (): Promise<A> {
218     return new Promise<A>((resolve: (arg: A) => void, reject: (err: any) => void) => {
219       func.apply(null, [ (err: any, res: A) => err ? reject(err) : resolve(res) ])
220     })
221   }
222 }
223
224 // Thanks to https://gist.github.com/kumasento/617daa7e46f13ecdd9b2
225 function promisify1<T, A> (func: (arg: T, cb: (err: any, result: A) => void) => void): (arg: T) => Promise<A> {
226   return function promisified (arg: T): Promise<A> {
227     return new Promise<A>((resolve: (arg: A) => void, reject: (err: any) => void) => {
228       func.apply(null, [ arg, (err: any, res: A) => err ? reject(err) : resolve(res) ])
229     })
230   }
231 }
232
233 function promisify2<T, U, A> (func: (arg1: T, arg2: U, cb: (err: any, result: A) => void) => void): (arg1: T, arg2: U) => Promise<A> {
234   return function promisified (arg1: T, arg2: U): Promise<A> {
235     return new Promise<A>((resolve: (arg: A) => void, reject: (err: any) => void) => {
236       func.apply(null, [ arg1, arg2, (err: any, res: A) => err ? reject(err) : resolve(res) ])
237     })
238   }
239 }
240
241 const randomBytesPromise = promisify1<number, Buffer>(randomBytes)
242 const createPrivateKey = promisify1<number, { key: string }>(pem.createPrivateKey)
243 const getPublicKey = promisify1<string, { publicKey: string }>(pem.getPublicKey)
244 const execPromise2 = promisify2<string, any, string>(exec)
245 const execPromise = promisify1<string, string>(exec)
246
247 // ---------------------------------------------------------------------------
248
249 export {
250   isTestInstance,
251   isProdInstance,
252   getAppNumber,
253
254   objectConverter,
255   root,
256   escapeHTML,
257   pageToStartAndCount,
258   sanitizeUrl,
259   sanitizeHost,
260   buildPath,
261   execShell,
262   peertubeTruncate,
263
264   sha256,
265   sha1,
266
267   promisify0,
268   promisify1,
269   promisify2,
270
271   randomBytesPromise,
272   createPrivateKey,
273   getPublicKey,
274   execPromise2,
275   execPromise
276 }