Fetch outbox when searching an actor
[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 * as bcrypt from 'bcrypt'
7 import * as createTorrent from 'create-torrent'
8 import { createHash, pseudoRandomBytes } from 'crypto'
9 import { copyFile, readdir, readFile, rename, stat, Stats, unlink, writeFile } from 'fs'
10 import * as mkdirp from 'mkdirp'
11 import { isAbsolute, join } from 'path'
12 import * as pem from 'pem'
13 import * as rimraf from 'rimraf'
14 import { URL } from 'url'
15 import { truncate } from 'lodash'
16
17 const timeTable = {
18   ms:           1,
19   second:       1000,
20   minute:       60000,
21   hour:         3600000,
22   day:          3600000 * 24,
23   week:         3600000 * 24 * 7,
24   month:        3600000 * 24 * 30
25 }
26 export function parseDuration (duration: number | string): number {
27   if (typeof duration === 'number') return duration
28
29   if (typeof duration === 'string') {
30     const split = duration.match(/^([\d\.,]+)\s?(\w+)$/)
31
32     if (split.length === 3) {
33       const len = parseFloat(split[1])
34       let unit = split[2].replace(/s$/i,'').toLowerCase()
35       if (unit === 'm') {
36         unit = 'ms'
37       }
38
39       return (len || 1) * (timeTable[unit] || 0)
40     }
41   }
42
43   throw new Error('Duration could not be properly parsed')
44 }
45
46 function sanitizeUrl (url: string) {
47   const urlObject = new URL(url)
48
49   if (urlObject.protocol === 'https:' && urlObject.port === '443') {
50     urlObject.port = ''
51   } else if (urlObject.protocol === 'http:' && urlObject.port === '80') {
52     urlObject.port = ''
53   }
54
55   return urlObject.href.replace(/\/$/, '')
56 }
57
58 // Don't import remote scheme from constants because we are in core utils
59 function sanitizeHost (host: string, remoteScheme: string) {
60   const toRemove = remoteScheme === 'https' ? 443 : 80
61
62   return host.replace(new RegExp(`:${toRemove}$`), '')
63 }
64
65 function isTestInstance () {
66   return process.env.NODE_ENV === 'test'
67 }
68
69 function root () {
70   // We are in /helpers/utils.js
71   const paths = [ __dirname, '..', '..' ]
72
73   // We are under /dist directory
74   if (process.mainModule && process.mainModule.filename.endsWith('.ts') === false) {
75     paths.push('..')
76   }
77
78   return join.apply(null, paths)
79 }
80
81 // Thanks: https://stackoverflow.com/a/12034334
82 function escapeHTML (stringParam) {
83   if (!stringParam) return ''
84
85   const entityMap = {
86     '&': '&',
87     '<': '&lt;',
88     '>': '&gt;',
89     '"': '&quot;',
90     '\'': '&#39;',
91     '/': '&#x2F;',
92     '`': '&#x60;',
93     '=': '&#x3D;'
94   }
95
96   return String(stringParam).replace(/[&<>"'`=\/]/g, s => entityMap[s])
97 }
98
99 function pageToStartAndCount (page: number, itemsPerPage: number) {
100   const start = (page - 1) * itemsPerPage
101
102   return { start, count: itemsPerPage }
103 }
104
105 function buildPath (path: string) {
106   if (isAbsolute(path)) return path
107
108   return join(root(), path)
109 }
110
111 // Consistent with .length, lodash truncate function is not
112 function peertubeTruncate (str: string, maxLength: number) {
113   const options = {
114     length: maxLength
115   }
116   const truncatedStr = truncate(str, options)
117
118   // The truncated string is okay, we can return it
119   if (truncatedStr.length <= maxLength) return truncatedStr
120
121   // Lodash takes into account all UTF characters, whereas String.prototype.length does not: some characters have a length of 2
122   // We always use the .length so we need to truncate more if needed
123   options.length -= truncatedStr.length - maxLength
124   return truncate(str, options)
125 }
126
127 function sha256 (str: string) {
128   return createHash('sha256').update(str).digest('hex')
129 }
130
131 function promisify0<A> (func: (cb: (err: any, result: A) => void) => void): () => Promise<A> {
132   return function promisified (): Promise<A> {
133     return new Promise<A>((resolve: (arg: A) => void, reject: (err: any) => void) => {
134       func.apply(null, [ (err: any, res: A) => err ? reject(err) : resolve(res) ])
135     })
136   }
137 }
138
139 // Thanks to https://gist.github.com/kumasento/617daa7e46f13ecdd9b2
140 function promisify1<T, A> (func: (arg: T, cb: (err: any, result: A) => void) => void): (arg: T) => Promise<A> {
141   return function promisified (arg: T): Promise<A> {
142     return new Promise<A>((resolve: (arg: A) => void, reject: (err: any) => void) => {
143       func.apply(null, [ arg, (err: any, res: A) => err ? reject(err) : resolve(res) ])
144     })
145   }
146 }
147
148 function promisify1WithVoid<T> (func: (arg: T, cb: (err: any) => void) => void): (arg: T) => Promise<void> {
149   return function promisified (arg: T): Promise<void> {
150     return new Promise<void>((resolve: () => void, reject: (err: any) => void) => {
151       func.apply(null, [ arg, (err: any) => err ? reject(err) : resolve() ])
152     })
153   }
154 }
155
156 function promisify2<T, U, A> (func: (arg1: T, arg2: U, cb: (err: any, result: A) => void) => void): (arg1: T, arg2: U) => Promise<A> {
157   return function promisified (arg1: T, arg2: U): Promise<A> {
158     return new Promise<A>((resolve: (arg: A) => void, reject: (err: any) => void) => {
159       func.apply(null, [ arg1, arg2, (err: any, res: A) => err ? reject(err) : resolve(res) ])
160     })
161   }
162 }
163
164 function promisify2WithVoid<T, U> (func: (arg1: T, arg2: U, cb: (err: any) => void) => void): (arg1: T, arg2: U) => Promise<void> {
165   return function promisified (arg1: T, arg2: U): Promise<void> {
166     return new Promise<void>((resolve: () => void, reject: (err: any) => void) => {
167       func.apply(null, [ arg1, arg2, (err: any) => err ? reject(err) : resolve() ])
168     })
169   }
170 }
171
172 const copyFilePromise = promisify2WithVoid<string, string>(copyFile)
173 const readFileBufferPromise = promisify1<string, Buffer>(readFile)
174 const unlinkPromise = promisify1WithVoid<string>(unlink)
175 const renamePromise = promisify2WithVoid<string, string>(rename)
176 const writeFilePromise = promisify2WithVoid<string, any>(writeFile)
177 const readdirPromise = promisify1<string, string[]>(readdir)
178 const mkdirpPromise = promisify1<string, string>(mkdirp)
179 // we cannot modify the Promise types, so we should make the promisify instance check mkdirp
180 const pseudoRandomBytesPromise = promisify1<number, Buffer>(pseudoRandomBytes)
181 const createPrivateKey = promisify1<number, { key: string }>(pem.createPrivateKey)
182 const getPublicKey = promisify1<string, { publicKey: string }>(pem.getPublicKey)
183 const bcryptComparePromise = promisify2<any, string, boolean>(bcrypt.compare)
184 const bcryptGenSaltPromise = promisify1<number, string>(bcrypt.genSalt)
185 const bcryptHashPromise = promisify2<any, string | number, string>(bcrypt.hash)
186 const createTorrentPromise = promisify2<string, any, any>(createTorrent)
187 const rimrafPromise = promisify1WithVoid<string>(rimraf)
188 const statPromise = promisify1<string, Stats>(stat)
189
190 // ---------------------------------------------------------------------------
191
192 export {
193   isTestInstance,
194   root,
195   escapeHTML,
196   pageToStartAndCount,
197   sanitizeUrl,
198   sanitizeHost,
199   buildPath,
200   peertubeTruncate,
201   sha256,
202
203   promisify0,
204   promisify1,
205
206   copyFilePromise,
207   readdirPromise,
208   readFileBufferPromise,
209   unlinkPromise,
210   renamePromise,
211   writeFilePromise,
212   mkdirpPromise,
213   pseudoRandomBytesPromise,
214   createPrivateKey,
215   getPublicKey,
216   bcryptComparePromise,
217   bcryptGenSaltPromise,
218   bcryptHashPromise,
219   createTorrentPromise,
220   rimrafPromise,
221   statPromise
222 }