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