Modules.js 5.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231
  1. const fs = require('fs')
  2. const Path = require('path')
  3. const async = require('async')
  4. const { promisify } = require('util')
  5. const Settings = require('@overleaf/settings')
  6. const Views = require('./Views')
  7. const _ = require('lodash')
  8. const MODULE_BASE_PATH = Path.join(__dirname, '/../../../modules')
  9. const _modules = []
  10. let _modulesLoaded = false
  11. const _hooks = {}
  12. const _middleware = {}
  13. let _viewIncludes = {}
  14. async function modules() {
  15. if (!_modulesLoaded) {
  16. await loadModules()
  17. }
  18. return _modules
  19. }
  20. async function loadModulesImpl() {
  21. const settingsCheckModuleCjs = Path.join(
  22. MODULE_BASE_PATH,
  23. 'settings-check',
  24. 'index.js'
  25. )
  26. const settingsCheckModuleEsm = Path.join(
  27. MODULE_BASE_PATH,
  28. 'settings-check',
  29. 'index.mjs'
  30. )
  31. if (fs.existsSync(settingsCheckModuleCjs)) {
  32. await import(settingsCheckModuleCjs)
  33. } else if (fs.existsSync(settingsCheckModuleEsm)) {
  34. await import(settingsCheckModuleEsm)
  35. }
  36. for (const moduleName of Settings.moduleImportSequence || []) {
  37. let path
  38. if (fs.existsSync(Path.join(MODULE_BASE_PATH, moduleName, 'index.mjs'))) {
  39. path = Path.join(MODULE_BASE_PATH, moduleName, 'index.mjs')
  40. } else {
  41. path = Path.join(MODULE_BASE_PATH, moduleName, 'index.js')
  42. }
  43. const module = await import(path)
  44. const loadedModule = module.default || module
  45. loadedModule.name = moduleName
  46. _modules.push(loadedModule)
  47. if (loadedModule.viewIncludes) {
  48. throw new Error(
  49. `${moduleName}: module.viewIncludes moved into Settings.viewIncludes`
  50. )
  51. }
  52. if (loadedModule.dependencies) {
  53. for (const dependency of loadedModule.dependencies) {
  54. if (!Settings.moduleImportSequence.includes(dependency)) {
  55. throw new Error(
  56. `Module '${dependency}' listed as a dependency of '${moduleName}' is missing in the moduleImportSequence. Please also verify that it is available in the current environment.`
  57. )
  58. }
  59. }
  60. }
  61. }
  62. _modulesLoaded = true
  63. await attachHooks()
  64. await attachMiddleware()
  65. }
  66. const loadModules = _.memoize(loadModulesImpl)
  67. async function applyRouter(webRouter, privateApiRouter, publicApiRouter) {
  68. for (const module of await modules()) {
  69. if (module.router && module.router.apply) {
  70. await module.router.apply(webRouter, privateApiRouter, publicApiRouter)
  71. }
  72. }
  73. }
  74. async function applyNonCsrfRouter(
  75. webRouter,
  76. privateApiRouter,
  77. publicApiRouter
  78. ) {
  79. for (const module of await modules()) {
  80. if (module.nonCsrfRouter != null) {
  81. module.nonCsrfRouter.apply(webRouter, privateApiRouter, publicApiRouter)
  82. }
  83. if (module.router && module.router.applyNonCsrfRouter) {
  84. module.router.applyNonCsrfRouter(
  85. webRouter,
  86. privateApiRouter,
  87. publicApiRouter
  88. )
  89. }
  90. }
  91. }
  92. async function start() {
  93. for (const module of await modules()) {
  94. await module.start?.()
  95. }
  96. }
  97. function loadViewIncludes(app) {
  98. _viewIncludes = Views.compileViewIncludes(app)
  99. }
  100. async function applyMiddleware(appOrRouter, middlewareName, options) {
  101. if (!middlewareName) {
  102. throw new Error(
  103. 'middleware name must be provided to register module middleware'
  104. )
  105. }
  106. for (const module of await modules()) {
  107. if (module[middlewareName]) {
  108. module[middlewareName](appOrRouter, options)
  109. }
  110. }
  111. }
  112. function moduleIncludes(view, locals) {
  113. const compiledPartials = _viewIncludes[view] || []
  114. let html = ''
  115. for (const compiledPartial of compiledPartials) {
  116. html += compiledPartial(locals)
  117. }
  118. return html
  119. }
  120. function moduleIncludesAvailable(view) {
  121. return (_viewIncludes[view] || []).length > 0
  122. }
  123. async function linkedFileAgentsIncludes() {
  124. const agents = {}
  125. for (const module of await modules()) {
  126. for (const name in module.linkedFileAgents) {
  127. const agentFunction = module.linkedFileAgents[name]
  128. agents[name] = agentFunction()
  129. }
  130. }
  131. return agents
  132. }
  133. async function attachHooks() {
  134. for (const module of await modules()) {
  135. for (const hook in module.hooks || {}) {
  136. const method = module.hooks[hook]
  137. attachHook(hook, method)
  138. }
  139. }
  140. }
  141. function attachHook(name, method) {
  142. if (_hooks[name] == null) {
  143. _hooks[name] = []
  144. }
  145. _hooks[name].push(method)
  146. }
  147. async function attachMiddleware() {
  148. for (const module of await modules()) {
  149. for (const middleware in module.middleware || {}) {
  150. const method = module.middleware[middleware]
  151. if (_middleware[middleware] == null) {
  152. _middleware[middleware] = []
  153. }
  154. _middleware[middleware].push(method)
  155. }
  156. }
  157. }
  158. function fireHook(name, ...rest) {
  159. const adjustedLength = Math.max(rest.length, 1)
  160. const args = rest.slice(0, adjustedLength - 1)
  161. const callback = rest[adjustedLength - 1]
  162. function fire() {
  163. const methods = _hooks[name] || []
  164. const callMethods = methods.map(method => cb => method(...args, cb))
  165. async.series(callMethods, function (error, results) {
  166. if (error) {
  167. return callback(error)
  168. }
  169. callback(null, results)
  170. })
  171. }
  172. // ensure that modules are loaded if we need to fire a hook
  173. // this can happen if a script calls a method that fires a hook
  174. if (!_modulesLoaded) {
  175. loadModules()
  176. .then(() => {
  177. fire()
  178. })
  179. .catch(err => callback(err))
  180. } else {
  181. fire()
  182. }
  183. }
  184. async function getMiddleware(name) {
  185. // ensure that modules are loaded if we need to call a middleware
  186. if (!_modulesLoaded) {
  187. await loadModules()
  188. }
  189. return _middleware[name] || []
  190. }
  191. module.exports = {
  192. applyNonCsrfRouter,
  193. applyRouter,
  194. linkedFileAgentsIncludes,
  195. loadViewIncludes,
  196. moduleIncludes,
  197. moduleIncludesAvailable,
  198. applyMiddleware,
  199. start,
  200. hooks: {
  201. attach: attachHook,
  202. fire: fireHook,
  203. },
  204. middleware: getMiddleware,
  205. promises: {
  206. hooks: {
  207. fire: promisify(fireHook),
  208. },
  209. },
  210. }