Modules.js 4.9 KB

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