You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

433 lines
14 KiB

6 years ago
6 years ago
6 years ago
6 years ago
6 years ago
6 years ago
6 years ago
  1. const _ = require('lodash')
  2. const Promise = require('bluebird')
  3. const getos = Promise.promisify(require('getos'))
  4. const os = require('os')
  5. const filesize = require('filesize')
  6. const path = require('path')
  7. const fs = require('fs-extra')
  8. const moment = require('moment')
  9. const graphHelper = require('../../helpers/graph')
  10. const request = require('request-promise')
  11. const crypto = require('crypto')
  12. const nanoid = require('nanoid/non-secure').customAlphabet('1234567890abcdef', 10)
  13. /* global WIKI */
  14. const dbTypes = {
  15. mysql: 'MySQL',
  16. mariadb: 'MariaDB',
  17. postgres: 'PostgreSQL',
  18. sqlite: 'SQLite',
  19. mssql: 'MS SQL Server'
  20. }
  21. module.exports = {
  22. Query: {
  23. async system () { return {} }
  24. },
  25. Mutation: {
  26. async system () { return {} }
  27. },
  28. SystemQuery: {
  29. flags () {
  30. return _.transform(WIKI.config.flags, (result, value, key) => {
  31. result.push({ key, value })
  32. }, [])
  33. },
  34. async info () { return {} },
  35. async extensions () {
  36. const exts = Object.values(WIKI.extensions.ext).map(ext => _.pick(ext, ['key', 'title', 'description', 'isInstalled']))
  37. for (let ext of exts) {
  38. ext.isCompatible = await WIKI.extensions.ext[ext.key].isCompatible()
  39. }
  40. return exts
  41. },
  42. async exportStatus () {
  43. return {
  44. status: WIKI.system.exportStatus.status,
  45. progress: Math.ceil(WIKI.system.exportStatus.progress),
  46. message: WIKI.system.exportStatus.message,
  47. startedAt: WIKI.system.exportStatus.startedAt
  48. }
  49. }
  50. },
  51. SystemMutation: {
  52. async updateFlags (obj, args, context) {
  53. WIKI.config.flags = _.transform(args.flags, (result, row) => {
  54. _.set(result, row.key, row.value)
  55. }, {})
  56. await WIKI.configSvc.applyFlags()
  57. await WIKI.configSvc.saveToDb(['flags'])
  58. return {
  59. responseResult: graphHelper.generateSuccess('System Flags applied successfully')
  60. }
  61. },
  62. async resetTelemetryClientId (obj, args, context) {
  63. try {
  64. WIKI.telemetry.generateClientId()
  65. await WIKI.configSvc.saveToDb(['telemetry'])
  66. return {
  67. responseResult: graphHelper.generateSuccess('Telemetry state updated successfully')
  68. }
  69. } catch (err) {
  70. return graphHelper.generateError(err)
  71. }
  72. },
  73. async setTelemetry (obj, args, context) {
  74. try {
  75. _.set(WIKI.config, 'telemetry.isEnabled', args.enabled)
  76. WIKI.telemetry.enabled = args.enabled
  77. await WIKI.configSvc.saveToDb(['telemetry'])
  78. return {
  79. responseResult: graphHelper.generateSuccess('Telemetry Client ID has been reset successfully')
  80. }
  81. } catch (err) {
  82. return graphHelper.generateError(err)
  83. }
  84. },
  85. async performUpgrade (obj, args, context) {
  86. try {
  87. if (process.env.UPGRADE_COMPANION) {
  88. await request({
  89. method: 'POST',
  90. uri: 'http://wiki-update-companion/upgrade',
  91. qs: {
  92. ...process.env.UPGRADE_COMPANION_REF && { container: process.env.UPGRADE_COMPANION_REF }
  93. }
  94. })
  95. return {
  96. responseResult: graphHelper.generateSuccess('Upgrade has started.')
  97. }
  98. } else {
  99. throw new Error('You must run the wiki-update-companion container and pass the UPGRADE_COMPANION env var in order to use this feature.')
  100. }
  101. } catch (err) {
  102. return graphHelper.generateError(err)
  103. }
  104. },
  105. /**
  106. * Import Users from a v1 installation
  107. */
  108. async importUsersFromV1(obj, args, context) {
  109. try {
  110. const MongoClient = require('mongodb').MongoClient
  111. if (args.mongoDbConnString && args.mongoDbConnString.length > 10) {
  112. // -> Connect to DB
  113. const client = await MongoClient.connect(args.mongoDbConnString, {
  114. appname: `Wiki.js ${WIKI.version} Migration Tool`
  115. })
  116. const dbUsers = client.db().collection('users')
  117. const userCursor = dbUsers.find({ email: { '$ne': 'guest' } })
  118. const curDateISO = new Date().toISOString()
  119. let failed = []
  120. let usersCount = 0
  121. let groupsCount = 0
  122. let assignableGroups = []
  123. let reuseGroups = []
  124. // -> Create SINGLE group
  125. if (args.groupMode === `SINGLE`) {
  126. const singleGroup = await WIKI.models.groups.query().insert({
  127. name: `Import_${curDateISO}`,
  128. permissions: JSON.stringify(WIKI.data.groups.defaultPermissions),
  129. pageRules: JSON.stringify(WIKI.data.groups.defaultPageRules)
  130. })
  131. groupsCount++
  132. assignableGroups.push(singleGroup.id)
  133. }
  134. // -> Iterate all users
  135. while (await userCursor.hasNext()) {
  136. const usr = await userCursor.next()
  137. let usrGroup = []
  138. if (args.groupMode === `MULTI`) {
  139. // -> Check if global admin
  140. if (_.some(usr.rights, ['role', 'admin'])) {
  141. usrGroup.push(1)
  142. } else {
  143. // -> Check if identical group already exists
  144. const currentRights = _.sortBy(_.map(usr.rights, r => _.pick(r, ['role', 'path', 'exact', 'deny'])), ['role', 'path', 'exact', 'deny'])
  145. const ruleSetId = crypto.createHash('sha1').update(JSON.stringify(currentRights)).digest('base64')
  146. const existingGroup = _.find(reuseGroups, ['hash', ruleSetId])
  147. if (existingGroup) {
  148. usrGroup.push(existingGroup.groupId)
  149. } else {
  150. // -> Build new group
  151. const pageRules = _.map(usr.rights, r => {
  152. let roles = ['read:pages', 'read:assets', 'read:comments', 'write:comments']
  153. if (r.role === `write`) {
  154. roles = _.concat(roles, ['write:pages', 'manage:pages', 'read:source', 'read:history', 'write:assets', 'manage:assets'])
  155. }
  156. return {
  157. id: nanoid(),
  158. roles: roles,
  159. match: r.exact ? 'EXACT' : 'START',
  160. deny: r.deny,
  161. path: (r.path.indexOf('/') === 0) ? r.path.substring(1) : r.path,
  162. locales: []
  163. }
  164. })
  165. const perms = _.chain(pageRules).reject('deny').map('roles').union().flatten().value()
  166. // -> Create new group
  167. const newGroup = await WIKI.models.groups.query().insert({
  168. name: `Import_${curDateISO}_${groupsCount + 1}`,
  169. permissions: JSON.stringify(perms),
  170. pageRules: JSON.stringify(pageRules)
  171. })
  172. reuseGroups.push({
  173. groupId: newGroup.id,
  174. hash: ruleSetId
  175. })
  176. groupsCount++
  177. usrGroup.push(newGroup.id)
  178. }
  179. }
  180. }
  181. // -> Create User
  182. try {
  183. await WIKI.models.users.createNewUser({
  184. providerKey: usr.provider,
  185. email: usr.email,
  186. name: usr.name,
  187. passwordRaw: usr.password,
  188. groups: (usrGroup.length > 0) ? usrGroup : assignableGroups,
  189. mustChangePassword: false,
  190. sendWelcomeEmail: false
  191. })
  192. usersCount++
  193. } catch (err) {
  194. failed.push({
  195. provider: usr.provider,
  196. email: usr.email,
  197. error: err.message
  198. })
  199. WIKI.logger.warn(`${usr.email}: ${err}`)
  200. }
  201. }
  202. // -> Reload group permissions
  203. if (args.groupMode !== `NONE`) {
  204. await WIKI.auth.reloadGroups()
  205. WIKI.events.outbound.emit('reloadGroups')
  206. }
  207. client.close()
  208. return {
  209. responseResult: graphHelper.generateSuccess('Import completed.'),
  210. usersCount: usersCount,
  211. groupsCount: groupsCount,
  212. failed: failed
  213. }
  214. } else {
  215. throw new Error('MongoDB Connection String is missing or invalid.')
  216. }
  217. } catch (err) {
  218. return graphHelper.generateError(err)
  219. }
  220. },
  221. /**
  222. * Set HTTPS Redirection State
  223. */
  224. async setHTTPSRedirection (obj, args, context) {
  225. _.set(WIKI.config, 'server.sslRedir', args.enabled)
  226. await WIKI.configSvc.saveToDb(['server'])
  227. return {
  228. responseResult: graphHelper.generateSuccess('HTTP Redirection state set successfully.')
  229. }
  230. },
  231. /**
  232. * Renew SSL Certificate
  233. */
  234. async renewHTTPSCertificate (obj, args, context) {
  235. try {
  236. if (!WIKI.config.ssl.enabled) {
  237. throw new WIKI.Error.SystemSSLDisabled()
  238. } else if (WIKI.config.ssl.provider !== `letsencrypt`) {
  239. throw new WIKI.Error.SystemSSLRenewInvalidProvider()
  240. } else if (!WIKI.servers.le) {
  241. throw new WIKI.Error.SystemSSLLEUnavailable()
  242. } else {
  243. await WIKI.servers.le.requestCertificate()
  244. await WIKI.servers.restartServer('https')
  245. return {
  246. responseResult: graphHelper.generateSuccess('SSL Certificate renewed successfully.')
  247. }
  248. }
  249. } catch (err) {
  250. return graphHelper.generateError(err)
  251. }
  252. },
  253. /**
  254. * Export Wiki to Disk
  255. */
  256. async export (obj, args, context) {
  257. try {
  258. const desiredPath = path.resolve(WIKI.ROOTPATH, args.path)
  259. // -> Check if export process is already running
  260. if (WIKI.system.exportStatus.status === 'running') {
  261. throw new Error('Another export is already running.')
  262. }
  263. // -> Validate entities
  264. if (args.entities.length < 1) {
  265. throw new Error('Must specify at least 1 entity to export.')
  266. }
  267. // -> Check target path
  268. await fs.ensureDir(desiredPath)
  269. const existingFiles = await fs.readdir(desiredPath)
  270. if (existingFiles.length) {
  271. throw new Error('Target directory must be empty!')
  272. }
  273. // -> Start export
  274. WIKI.system.export({
  275. entities: args.entities,
  276. path: desiredPath
  277. })
  278. return {
  279. responseResult: graphHelper.generateSuccess('Export started successfully.')
  280. }
  281. } catch (err) {
  282. return graphHelper.generateError(err)
  283. }
  284. }
  285. },
  286. SystemInfo: {
  287. configFile () {
  288. return path.join(process.cwd(), 'config.yml')
  289. },
  290. cpuCores () {
  291. return os.cpus().length
  292. },
  293. currentVersion () {
  294. return WIKI.version
  295. },
  296. dbType () {
  297. return _.get(dbTypes, WIKI.config.db.type, 'Unknown DB')
  298. },
  299. async dbVersion () {
  300. let version = 'Unknown Version'
  301. switch (WIKI.config.db.type) {
  302. case 'mariadb':
  303. case 'mysql':
  304. const resultMYSQL = await WIKI.models.knex.raw('SELECT VERSION() as version;')
  305. version = _.get(resultMYSQL, '[0][0].version', 'Unknown Version')
  306. break
  307. case 'mssql':
  308. const resultMSSQL = await WIKI.models.knex.raw('SELECT @@VERSION as version;')
  309. version = _.get(resultMSSQL, '[0].version', 'Unknown Version')
  310. break
  311. case 'postgres':
  312. version = _.get(WIKI.models, 'knex.client.version', 'Unknown Version')
  313. break
  314. case 'sqlite':
  315. version = _.get(WIKI.models, 'knex.client.driver.VERSION', 'Unknown Version')
  316. break
  317. }
  318. return version
  319. },
  320. dbHost () {
  321. if (WIKI.config.db.type === 'sqlite') {
  322. return WIKI.config.db.storage
  323. } else {
  324. return WIKI.config.db.host
  325. }
  326. },
  327. hostname () {
  328. return os.hostname()
  329. },
  330. httpPort () {
  331. return WIKI.servers.servers.http ? _.get(WIKI.servers.servers.http.address(), 'port', 0) : 0
  332. },
  333. httpRedirection () {
  334. return _.get(WIKI.config, 'server.sslRedir', false)
  335. },
  336. httpsPort () {
  337. return WIKI.servers.servers.https ? _.get(WIKI.servers.servers.https.address(), 'port', 0) : 0
  338. },
  339. latestVersion () {
  340. return WIKI.system.updates.version
  341. },
  342. latestVersionReleaseDate () {
  343. return moment.utc(WIKI.system.updates.releaseDate)
  344. },
  345. nodeVersion () {
  346. return process.version.substr(1)
  347. },
  348. async operatingSystem () {
  349. let osLabel = `${os.type()} (${os.platform()}) ${os.release()} ${os.arch()}`
  350. if (os.platform() === 'linux') {
  351. const osInfo = await getos()
  352. osLabel = `${os.type()} - ${osInfo.dist} (${osInfo.codename || os.platform()}) ${osInfo.release || os.release()} ${os.arch()}`
  353. }
  354. return osLabel
  355. },
  356. async platform () {
  357. const isDockerized = await fs.pathExists('/.dockerenv')
  358. if (isDockerized) {
  359. return 'docker'
  360. }
  361. return os.platform()
  362. },
  363. ramTotal () {
  364. return filesize(os.totalmem())
  365. },
  366. sslDomain () {
  367. return WIKI.config.ssl.enabled && WIKI.config.ssl.provider === `letsencrypt` ? WIKI.config.ssl.domain : null
  368. },
  369. sslExpirationDate () {
  370. return WIKI.config.ssl.enabled && WIKI.config.ssl.provider === `letsencrypt` ? _.get(WIKI.config.letsencrypt, 'payload.expires', null) : null
  371. },
  372. sslProvider () {
  373. return WIKI.config.ssl.enabled ? WIKI.config.ssl.provider : null
  374. },
  375. sslStatus () {
  376. return 'OK'
  377. },
  378. sslSubscriberEmail () {
  379. return WIKI.config.ssl.enabled && WIKI.config.ssl.provider === `letsencrypt` ? WIKI.config.ssl.subscriberEmail : null
  380. },
  381. telemetry () {
  382. return WIKI.telemetry.enabled
  383. },
  384. telemetryClientId () {
  385. return WIKI.config.telemetry.clientId
  386. },
  387. async upgradeCapable () {
  388. return !_.isNil(process.env.UPGRADE_COMPANION)
  389. },
  390. workingDirectory () {
  391. return process.cwd()
  392. },
  393. async groupsTotal () {
  394. const total = await WIKI.models.groups.query().count('* as total').first()
  395. return _.toSafeInteger(total.total)
  396. },
  397. async pagesTotal () {
  398. const total = await WIKI.models.pages.query().count('* as total').first()
  399. return _.toSafeInteger(total.total)
  400. },
  401. async usersTotal () {
  402. const total = await WIKI.models.users.query().count('* as total').first()
  403. return _.toSafeInteger(total.total)
  404. },
  405. async tagsTotal () {
  406. const total = await WIKI.models.tags.query().count('* as total').first()
  407. return _.toSafeInteger(total.total)
  408. }
  409. }
  410. }