wiki-server 0.25.10 → 0.26.0-rc.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AUTHORS.txt +1 -0
- package/index.js +3 -7
- package/lib/defaultargs.js +83 -0
- package/lib/forward.js +10 -10
- package/lib/page.js +448 -0
- package/lib/plugins.js +54 -0
- package/lib/random_id.js +15 -0
- package/lib/search.js +390 -0
- package/lib/security.js +94 -0
- package/lib/server.js +942 -0
- package/lib/sitemap.js +288 -0
- package/package.json +32 -23
- package/.github/workflows/node.js.yml +0 -31
- package/.mailmap +0 -1
- package/Gruntfile.js +0 -55
- package/lib/defaultargs.coffee +0 -77
- package/lib/page.coffee +0 -371
- package/lib/plugins.coffee +0 -48
- package/lib/random_id.coffee +0 -18
- package/lib/search.coffee +0 -304
- package/lib/security.coffee +0 -86
- package/lib/server.coffee +0 -782
- package/lib/sitemap.coffee +0 -246
- package/test/asdf-test-page +0 -8
- package/test/defaultargs.coffee +0 -10
- package/test/mocha.opts +0 -2
- package/test/page.coffee +0 -61
- package/test/random.coffee +0 -8
- package/test/server.coffee +0 -228
- package/test/sitemap.coffee +0 -110
package/AUTHORS.txt
CHANGED
package/index.js
CHANGED
|
@@ -2,11 +2,7 @@
|
|
|
2
2
|
// Simple file so that if you require this directory
|
|
3
3
|
// in node it instead requires ./lib/server.coffee
|
|
4
4
|
// with coffee-script already loaded.
|
|
5
|
-
require('coffeescript')
|
|
6
|
-
require('coffeescript/register')
|
|
5
|
+
require('coffeescript')
|
|
6
|
+
require('coffeescript/register')
|
|
7
7
|
|
|
8
|
-
|
|
9
|
-
// part of a reload, and an exit after an uncaught error
|
|
10
|
-
process.exitCode = 1
|
|
11
|
-
|
|
12
|
-
module.exports = require('./lib/server');
|
|
8
|
+
module.exports = require('./lib/server')
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Federated Wiki : Node Server
|
|
3
|
+
*
|
|
4
|
+
* Copyright Ward Cunningham and other contributors
|
|
5
|
+
* Licensed under the MIT license.
|
|
6
|
+
* https://github.com/fedwiki/wiki-server/blob/master/LICENSE.txt
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
// **defaultargs.coffee** when called on the argv object this
|
|
10
|
+
// module will create reasonable defaults for options not supplied,
|
|
11
|
+
// based on what information is provided.
|
|
12
|
+
const path = require('node:path')
|
|
13
|
+
|
|
14
|
+
const getUserHome = () => {
|
|
15
|
+
return process.env.HOME || process.env.HOMEPATH || process.env.USERPROFILE
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
module.exports = argv => {
|
|
19
|
+
argv = argv || {}
|
|
20
|
+
argv.root ||= __dirname
|
|
21
|
+
// the directory that contains all the packages that makeup the wiki
|
|
22
|
+
argv.packageDir ||= path.join(argv.root, '..')
|
|
23
|
+
argv.port ||= 3000
|
|
24
|
+
argv.home ||= 'welcome-visitors'
|
|
25
|
+
argv.data ||= path.join(getUserHome(), '.wiki') // see also cli
|
|
26
|
+
argv.client ||= path.join(argv.packageDir, 'wiki-client', 'client')
|
|
27
|
+
argv.db ||= path.join(argv.data, 'pages')
|
|
28
|
+
argv.status ||= path.join(argv.data, 'status')
|
|
29
|
+
argv.assets ||= path.join(argv.data, 'assets')
|
|
30
|
+
argv.recycler ||= path.join(argv.data, 'recycle')
|
|
31
|
+
argv.commons ||= path.join(argv.data, 'commons')
|
|
32
|
+
argv.url ||= `http://localhost${argv.port === 80 ? '' : ':' + argv.port}`
|
|
33
|
+
argv.id ||= path.join(argv.status, 'owner.json')
|
|
34
|
+
argv.uploadLimit ||= '5mb'
|
|
35
|
+
argv.cookieSecret ||= require('crypto').randomBytes(64).toString('hex')
|
|
36
|
+
argv.secure_cookie ||= false
|
|
37
|
+
argv.session_duration ||= 7
|
|
38
|
+
argv.neighbors ||= ''
|
|
39
|
+
argv.debug ||= false
|
|
40
|
+
argv.test ||= false
|
|
41
|
+
|
|
42
|
+
if (typeof argv.database === 'string') {
|
|
43
|
+
argv.database = JSON.parse(argv.database)
|
|
44
|
+
}
|
|
45
|
+
argv.database ||= {}
|
|
46
|
+
argv.database.type ||= './page'
|
|
47
|
+
if (argv.database.type.charAt(0) === '.') {
|
|
48
|
+
if (argv.database.type != './page') {
|
|
49
|
+
console.log('\n\nWARNING: This storage option is depeciated.')
|
|
50
|
+
console.log(' See ReadMe for details of the changes required.\n\n')
|
|
51
|
+
}
|
|
52
|
+
} else {
|
|
53
|
+
argv.database.type = 'wiki-storage-' + argv.database.type
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
argv.security_type ||= './security'
|
|
57
|
+
if (argv.security_type === './security') {
|
|
58
|
+
console.log('\n\nINFORMATION: Using default security module.')
|
|
59
|
+
} else {
|
|
60
|
+
argv.security_type = 'wiki-security-' + argv.security_type
|
|
61
|
+
}
|
|
62
|
+
argv.security_legacy ||= false
|
|
63
|
+
|
|
64
|
+
// resolve all relative paths
|
|
65
|
+
argv.root = path.resolve(argv.root)
|
|
66
|
+
argv.packageDir = path.resolve(argv.packageDir)
|
|
67
|
+
argv.data = path.resolve(argv.data)
|
|
68
|
+
argv.client = path.resolve(argv.client)
|
|
69
|
+
argv.db = path.resolve(argv.db)
|
|
70
|
+
argv.status = path.resolve(argv.status)
|
|
71
|
+
argv.assets = path.resolve(argv.assets)
|
|
72
|
+
argv.recycler = path.resolve(argv.recycler)
|
|
73
|
+
argv.commons = path.resolve(argv.commons)
|
|
74
|
+
argv.id = path.resolve(argv.id)
|
|
75
|
+
|
|
76
|
+
if (/node_modules/.test(argv.data)) {
|
|
77
|
+
console.log('\n\nWARNING : The dafault data path is not a safe place.')
|
|
78
|
+
console.log(' : by using ', argv.data, ' your pages will be lost when packages are updated.')
|
|
79
|
+
console.log(' : You are strongly advised to use an alternative directory.')
|
|
80
|
+
console.log(' : See the wiki package ReadMe for how to do this.\n\n')
|
|
81
|
+
}
|
|
82
|
+
return argv
|
|
83
|
+
}
|
package/lib/forward.js
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
const init = (app, emitter) => {
|
|
2
2
|
let sockets = []
|
|
3
|
-
app.io.on('connection',
|
|
3
|
+
app.io.on('connection', socket => {
|
|
4
4
|
let listeners = []
|
|
5
5
|
sockets.push(socket)
|
|
6
6
|
console.log('client connected:', socket.handshake.address)
|
|
7
|
-
socket.on('disconnect',
|
|
7
|
+
socket.on('disconnect', reason => {
|
|
8
8
|
console.log('client disconnected:', socket.handshake.address, reason)
|
|
9
|
-
for (let {sProducer, listener} of listeners) {
|
|
9
|
+
for (let { sProducer, listener } of listeners) {
|
|
10
10
|
console.log('removing listener:', sProducer)
|
|
11
11
|
emitter.removeListener(sProducer, listener)
|
|
12
12
|
}
|
|
@@ -14,9 +14,9 @@ const init = (app, emitter) => {
|
|
|
14
14
|
let i = sockets.indexOf(socket)
|
|
15
15
|
sockets.splice(i, 1)
|
|
16
16
|
})
|
|
17
|
-
socket.on('unsubscribe',
|
|
17
|
+
socket.on('unsubscribe', sProducer => {
|
|
18
18
|
console.log('unsubscribing listener:', socket.handshake.address, sProducer)
|
|
19
|
-
for (let [i, {slugItem, listener}] of listeners.entries()) {
|
|
19
|
+
for (let [i, { slugItem, listener }] of listeners.entries()) {
|
|
20
20
|
if (slugItem == sProducer) {
|
|
21
21
|
console.log('removing listener:', sProducer)
|
|
22
22
|
emitter.removeListener(sProducer, listener)
|
|
@@ -24,15 +24,15 @@ const init = (app, emitter) => {
|
|
|
24
24
|
}
|
|
25
25
|
}
|
|
26
26
|
})
|
|
27
|
-
socket.on('subscribe',
|
|
28
|
-
let listener =
|
|
27
|
+
socket.on('subscribe', sProducer => {
|
|
28
|
+
let listener = result => {
|
|
29
29
|
console.log('forwarding:', socket.handshake.address, result)
|
|
30
|
-
socket.emit(sProducer, {slugItem: sProducer, result})
|
|
30
|
+
socket.emit(sProducer, { slugItem: sProducer, result })
|
|
31
31
|
}
|
|
32
32
|
console.log(`registering listener:`, socket.handshake.address, sProducer)
|
|
33
33
|
emitter.on(sProducer, listener)
|
|
34
|
-
listeners.push({sProducer, listener})
|
|
34
|
+
listeners.push({ sProducer, listener })
|
|
35
35
|
})
|
|
36
36
|
})
|
|
37
37
|
}
|
|
38
|
-
module.exports = {init}
|
|
38
|
+
module.exports = { init }
|
package/lib/page.js
ADDED
|
@@ -0,0 +1,448 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Federated Wiki : Node Server
|
|
3
|
+
*
|
|
4
|
+
* Copyright Ward Cunningham and other contributors
|
|
5
|
+
* Licensed under the MIT license.
|
|
6
|
+
* https://github.com/fedwiki/wiki-server/blob/master/LICENSE.txt
|
|
7
|
+
*/
|
|
8
|
+
// **page.coffee**
|
|
9
|
+
// Module for interacting with pages persisted on the server.
|
|
10
|
+
// Everything is stored using json flat files.
|
|
11
|
+
|
|
12
|
+
// #### Requires ####
|
|
13
|
+
const fs = require('fs')
|
|
14
|
+
const path = require('path')
|
|
15
|
+
const events = require('events')
|
|
16
|
+
const glob = require('glob')
|
|
17
|
+
|
|
18
|
+
const async = require('async')
|
|
19
|
+
|
|
20
|
+
const random_id = require('./random_id')
|
|
21
|
+
const synopsis = require('wiki-client/lib/synopsis')
|
|
22
|
+
|
|
23
|
+
const asSlug = name =>
|
|
24
|
+
name
|
|
25
|
+
.replace(/\s/g, '-')
|
|
26
|
+
.replace(/[^A-Za-z0-9-]/g, '')
|
|
27
|
+
.toLowerCase()
|
|
28
|
+
|
|
29
|
+
// Export a function that generates a page handler
|
|
30
|
+
// when called with options object.
|
|
31
|
+
module.exports = exports = argv => {
|
|
32
|
+
const wikiName = new URL(argv.url).hostname
|
|
33
|
+
|
|
34
|
+
fs.mkdir(argv.db, { recursive: true }, e => {
|
|
35
|
+
if (e) throw e
|
|
36
|
+
})
|
|
37
|
+
|
|
38
|
+
// #### Private utility methods. ####
|
|
39
|
+
const load_parse = (loc, cb, annotations = {}) => {
|
|
40
|
+
let page
|
|
41
|
+
fs.readFile(loc, (err, data) => {
|
|
42
|
+
if (err) return cb(err)
|
|
43
|
+
try {
|
|
44
|
+
page = JSON.parse(data)
|
|
45
|
+
} catch {
|
|
46
|
+
const errorPage = path.basename(loc)
|
|
47
|
+
const errorPagePath = path.dirname(loc)
|
|
48
|
+
const recyclePage = path.resolve(errorPagePath, '..', 'recycle', errorPage)
|
|
49
|
+
// TODO: fs.exists depreciated
|
|
50
|
+
fs.exists(path.dirname(recyclePage), exists => {
|
|
51
|
+
if (exists) {
|
|
52
|
+
fs.rename(loc, recyclePage, err => {
|
|
53
|
+
if (err) {
|
|
54
|
+
console.log('ERROR: moving problem page #{loc} to recycler', err)
|
|
55
|
+
} else {
|
|
56
|
+
console.log('ERROR: problem page #{loc} moved to recycler')
|
|
57
|
+
}
|
|
58
|
+
})
|
|
59
|
+
} else {
|
|
60
|
+
fs.mkdir(path.dirname(recyclePage), { recursive: true }, err => {
|
|
61
|
+
if (err) {
|
|
62
|
+
console.log('ERROR: creating recycler', err)
|
|
63
|
+
} else {
|
|
64
|
+
fs.rename(loc, recyclePage, err => {
|
|
65
|
+
if (err) {
|
|
66
|
+
console.log('ERROR: moving problem page #{loc} to recycler', err)
|
|
67
|
+
} else {
|
|
68
|
+
console.log('ERROR: problem page #{loc} moved to recycler')
|
|
69
|
+
}
|
|
70
|
+
})
|
|
71
|
+
}
|
|
72
|
+
})
|
|
73
|
+
}
|
|
74
|
+
})
|
|
75
|
+
|
|
76
|
+
return cb(null, 'Error Parsing Page', 404)
|
|
77
|
+
}
|
|
78
|
+
for (const [key, val] of Object.entries(annotations)) {
|
|
79
|
+
page[key] = val
|
|
80
|
+
}
|
|
81
|
+
cb(null, page)
|
|
82
|
+
})
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const load_parse_copy = (defloc, file, cb) => {
|
|
86
|
+
fs.readFile(defloc, (err, data) => {
|
|
87
|
+
if (err) cb(err)
|
|
88
|
+
let page
|
|
89
|
+
try {
|
|
90
|
+
page = JSON.parse(data)
|
|
91
|
+
} catch (e) {
|
|
92
|
+
return cb(e)
|
|
93
|
+
}
|
|
94
|
+
cb(null, page)
|
|
95
|
+
// TODO: what is happening here?! put will never be reached???
|
|
96
|
+
itself.put(file, page, err => {
|
|
97
|
+
if (err) cb(err)
|
|
98
|
+
})
|
|
99
|
+
})
|
|
100
|
+
}
|
|
101
|
+
// Reads and writes are async, but serially queued to avoid race conditions.
|
|
102
|
+
const queue = []
|
|
103
|
+
|
|
104
|
+
const tryDefaults = (file, cb) => {
|
|
105
|
+
const lastDefault = cb => {
|
|
106
|
+
const defloc = path.join(argv.root, 'default-data', 'pages', file)
|
|
107
|
+
// TODO: fs.exists depreciated
|
|
108
|
+
fs.exists(defloc, exists => {
|
|
109
|
+
if (exists) {
|
|
110
|
+
cb(defloc)
|
|
111
|
+
} else {
|
|
112
|
+
cb(null)
|
|
113
|
+
}
|
|
114
|
+
})
|
|
115
|
+
}
|
|
116
|
+
if (argv.defaults) {
|
|
117
|
+
const defloc = path.join(argv.data, '..', argv.defaults, 'pages', file)
|
|
118
|
+
console.log('firstDefault', defloc)
|
|
119
|
+
// TODO: fs.exists depreciated
|
|
120
|
+
fs.exists(defloc, exists => {
|
|
121
|
+
if (exists) {
|
|
122
|
+
cb(defloc)
|
|
123
|
+
} else {
|
|
124
|
+
lastDefault(cb)
|
|
125
|
+
}
|
|
126
|
+
})
|
|
127
|
+
} else {
|
|
128
|
+
lastDefault(cb)
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// Main file io function, when called without page it reads,
|
|
133
|
+
// when called with page it writes.
|
|
134
|
+
const fileio = (action, file, page, cb) => {
|
|
135
|
+
const loc = file.startsWith('recycler/') ? path.join(argv.recycler, file.split('/')[1]) : path.join(argv.db, file)
|
|
136
|
+
|
|
137
|
+
switch (action) {
|
|
138
|
+
case 'delete':
|
|
139
|
+
if (file.startsWith('recycler/')) {
|
|
140
|
+
// delete from recycler
|
|
141
|
+
// TODO: fs.exists depreciated
|
|
142
|
+
fs.exists(loc, exists => {
|
|
143
|
+
if (exists)
|
|
144
|
+
fs.unlink(loc, err => {
|
|
145
|
+
cb(err)
|
|
146
|
+
})
|
|
147
|
+
})
|
|
148
|
+
} else {
|
|
149
|
+
// move page to recycler
|
|
150
|
+
// TODO: fs.exists depreciated
|
|
151
|
+
fs.exists(loc, exists => {
|
|
152
|
+
if (exists) {
|
|
153
|
+
const recycleLoc = path.join(argv.recycler, file)
|
|
154
|
+
// TODO: fs.exists depreciated
|
|
155
|
+
fs.exists(path.dirname(recycleLoc), exists => {
|
|
156
|
+
if (exists) {
|
|
157
|
+
fs.rename(loc, recycleLoc, err => {
|
|
158
|
+
cb(err)
|
|
159
|
+
})
|
|
160
|
+
} else {
|
|
161
|
+
fs.mkdir(path.dirname(recycleLoc), { recursive: true }, err => {
|
|
162
|
+
if (err) cb(err)
|
|
163
|
+
fs.rename(loc, recycleLoc, err => {
|
|
164
|
+
cb(err)
|
|
165
|
+
})
|
|
166
|
+
})
|
|
167
|
+
}
|
|
168
|
+
})
|
|
169
|
+
} else {
|
|
170
|
+
cb('page does not exist')
|
|
171
|
+
}
|
|
172
|
+
})
|
|
173
|
+
}
|
|
174
|
+
break
|
|
175
|
+
case 'recycle': {
|
|
176
|
+
const copyFile = (source, target, cb) => {
|
|
177
|
+
const done = err => {
|
|
178
|
+
if (!cbCalled) {
|
|
179
|
+
cb(err)
|
|
180
|
+
cbCalled = true
|
|
181
|
+
}
|
|
182
|
+
return
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
let cbCalled = false
|
|
186
|
+
|
|
187
|
+
const rd = fs.createReadStream(source)
|
|
188
|
+
rd.on('error', err => {
|
|
189
|
+
done(err)
|
|
190
|
+
return
|
|
191
|
+
})
|
|
192
|
+
|
|
193
|
+
const wr = fs.createWriteStream(target)
|
|
194
|
+
wr.on('error', err => {
|
|
195
|
+
done(err)
|
|
196
|
+
return
|
|
197
|
+
})
|
|
198
|
+
wr.on('close', () => {
|
|
199
|
+
done()
|
|
200
|
+
return
|
|
201
|
+
})
|
|
202
|
+
rd.pipe(wr)
|
|
203
|
+
return
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// TODO: fs.exists depreciated
|
|
207
|
+
fs.exists(loc, exists => {
|
|
208
|
+
if (exists) {
|
|
209
|
+
const recycleLoc = path.join(argv.recycler, file)
|
|
210
|
+
// TODO: fs.exists depreciated
|
|
211
|
+
fs.exists(path.dirname(recycleLoc), exists => {
|
|
212
|
+
if (exists) {
|
|
213
|
+
copyFile(loc, recycleLoc, err => {
|
|
214
|
+
cb(err)
|
|
215
|
+
})
|
|
216
|
+
} else {
|
|
217
|
+
fs.mkdir(path.dirname(recycleLoc), { recursive: true }, err => {
|
|
218
|
+
if (err) cb(err)
|
|
219
|
+
copyFile(loc, recycleLoc, err => {
|
|
220
|
+
cb(err)
|
|
221
|
+
})
|
|
222
|
+
})
|
|
223
|
+
}
|
|
224
|
+
})
|
|
225
|
+
} else {
|
|
226
|
+
cb('page does not exist')
|
|
227
|
+
}
|
|
228
|
+
})
|
|
229
|
+
break
|
|
230
|
+
}
|
|
231
|
+
case 'get':
|
|
232
|
+
// TODO: fs.exists depreciated
|
|
233
|
+
fs.exists(loc, exists => {
|
|
234
|
+
if (exists) {
|
|
235
|
+
load_parse(loc, cb, { plugin: undefined })
|
|
236
|
+
} else {
|
|
237
|
+
tryDefaults(file, defloc => {
|
|
238
|
+
if (defloc) {
|
|
239
|
+
load_parse(defloc, cb)
|
|
240
|
+
} else {
|
|
241
|
+
glob('wiki-plugin-*/pages', { cwd: argv.packageDir }, (e, plugins) => {
|
|
242
|
+
if (e) return cb(e)
|
|
243
|
+
|
|
244
|
+
// if no plugins found
|
|
245
|
+
if (plugins.length === 0) {
|
|
246
|
+
cb(null, 'Page not found', 404)
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
let count = plugins.length
|
|
250
|
+
const giveUp = () => {
|
|
251
|
+
count -= 1
|
|
252
|
+
if (count === 0) {
|
|
253
|
+
cb(null, 'Page not found', 404)
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
plugins.forEach(plugin => {
|
|
257
|
+
const pluginName = plugin.slice(12, -6)
|
|
258
|
+
const pluginloc = path.join(argv.packageDir, plugin, file)
|
|
259
|
+
fs.exists(pluginloc, exists => {
|
|
260
|
+
if (exists) {
|
|
261
|
+
load_parse(pluginloc, cb, { plugin: pluginName })
|
|
262
|
+
} else {
|
|
263
|
+
giveUp()
|
|
264
|
+
}
|
|
265
|
+
})
|
|
266
|
+
})
|
|
267
|
+
})
|
|
268
|
+
}
|
|
269
|
+
})
|
|
270
|
+
}
|
|
271
|
+
})
|
|
272
|
+
break
|
|
273
|
+
case 'put':
|
|
274
|
+
page = JSON.stringify(page, null, 2)
|
|
275
|
+
fs.exists(path.dirname(loc), exists => {
|
|
276
|
+
if (exists) {
|
|
277
|
+
fs.writeFile(loc, page, err => {
|
|
278
|
+
if (err) {
|
|
279
|
+
console.log('ERROR: write file #{loc} ', err)
|
|
280
|
+
}
|
|
281
|
+
cb(err)
|
|
282
|
+
})
|
|
283
|
+
} else {
|
|
284
|
+
fs.mkdir(path.dirname(loc), { recursive: true }, err => {
|
|
285
|
+
if (err) cb(err)
|
|
286
|
+
fs.writeFile(loc, page, err => {
|
|
287
|
+
if (err) {
|
|
288
|
+
console.log('ERROR: write file #{loc} ', err)
|
|
289
|
+
}
|
|
290
|
+
cb(err)
|
|
291
|
+
})
|
|
292
|
+
})
|
|
293
|
+
}
|
|
294
|
+
})
|
|
295
|
+
break
|
|
296
|
+
default:
|
|
297
|
+
console.log('pagehandler: unrecognized action #{action}')
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
// Control variable that tells if the serial queue is currently working.
|
|
302
|
+
// Set back to false when all jobs are complete.
|
|
303
|
+
let working = false
|
|
304
|
+
|
|
305
|
+
// Keep file io working on queued jobs, but don't block the main thread.
|
|
306
|
+
const serial = item => {
|
|
307
|
+
if (item) {
|
|
308
|
+
itself.start()
|
|
309
|
+
fileio(item.action, item.file, item.page, (err, data, status) => {
|
|
310
|
+
process.nextTick(() => {
|
|
311
|
+
serial(queue.shift())
|
|
312
|
+
})
|
|
313
|
+
item.cb(err, data, status)
|
|
314
|
+
})
|
|
315
|
+
} else {
|
|
316
|
+
itself.stop()
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
// #### Public stuff ####
|
|
321
|
+
// Make the exported object an instance of EventEmitter
|
|
322
|
+
// so other modules can tell if it is working or not.
|
|
323
|
+
const itself = new events.EventEmitter()
|
|
324
|
+
|
|
325
|
+
itself.start = () => {
|
|
326
|
+
working = true
|
|
327
|
+
itself.emit('working')
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
itself.stop = () => {
|
|
331
|
+
working = false
|
|
332
|
+
itself.emit('finished')
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
itself.isWorking = () => working
|
|
336
|
+
|
|
337
|
+
// get method takes a slug and a callback, adding them to the queue,
|
|
338
|
+
// starting serial if it isn't already working.
|
|
339
|
+
itself.get = (file, cb) => {
|
|
340
|
+
queue.push({ action: 'get', file, page: null, cb })
|
|
341
|
+
if (!working) serial(queue.shift())
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
// put takes a slugged name, the page as a json object, and a callback.
|
|
345
|
+
// adds them to the queue, and starts it unless it is working.
|
|
346
|
+
itself.put = (file, page, cb) => {
|
|
347
|
+
queue.push({ action: 'put', file, page, cb })
|
|
348
|
+
if (!working) serial(queue.shift())
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
itself.delete = (file, cb) => {
|
|
352
|
+
queue.push({ action: 'delete', file, page: null, cb })
|
|
353
|
+
if (!working) serial(queue.shift())
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
itself.saveToRecycler = (file, cb) => {
|
|
357
|
+
queue.push({ action: 'recycle', file, page: null, cb })
|
|
358
|
+
if (!working) serial(queue.shift())
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
const editDate = journal => {
|
|
362
|
+
if (!journal) return undefined
|
|
363
|
+
// find the last journal entry, that is not a fork, with a date.
|
|
364
|
+
const last = journal.findLast(action => {
|
|
365
|
+
return action.date && action.type != 'fork'
|
|
366
|
+
})
|
|
367
|
+
return last ? last.date : undefined
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
itself.pages = cb => {
|
|
371
|
+
const extractPageLinks = (collaborativeLinks, currentItem, currentIndex, array) => {
|
|
372
|
+
// extract collaborative links
|
|
373
|
+
// - this will need extending if we also extract the id of the item containing the link
|
|
374
|
+
try {
|
|
375
|
+
const linkRe = /\[\[([^\]]+)\]\]/g
|
|
376
|
+
let match = undefined
|
|
377
|
+
while ((match = linkRe.exec(currentItem.text)) != null) {
|
|
378
|
+
if (!collaborativeLinks.has(asSlug(match[1]))) {
|
|
379
|
+
collaborativeLinks.set(asSlug(match[1]), currentItem.id)
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
if ('reference' == currentItem.type) {
|
|
383
|
+
if (!collaborativeLinks.has(currentItem.slug)) {
|
|
384
|
+
collaborativeLinks.set(currentItem.slug, currentItem.id)
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
} catch (err) {
|
|
388
|
+
console.log(
|
|
389
|
+
`METADATA *** ${wikiName} Error extracting links from ${currentIndex} of ${JSON.stringify(array)}`,
|
|
390
|
+
err.message,
|
|
391
|
+
)
|
|
392
|
+
}
|
|
393
|
+
return collaborativeLinks
|
|
394
|
+
}
|
|
395
|
+
fs.readdir(argv.db, (e, files) => {
|
|
396
|
+
if (e) return cb(e)
|
|
397
|
+
// used to make sure all of the files are read
|
|
398
|
+
// and processesed in the site map before responding
|
|
399
|
+
const doSitemap = (file, cb) => {
|
|
400
|
+
itself.get(file, (e, page, status) => {
|
|
401
|
+
if (file.match(/^\./)) return cb()
|
|
402
|
+
if (e || status === 404) {
|
|
403
|
+
console.log('Problem building sitemap:', file, 'e: ', e, 'status:', status)
|
|
404
|
+
return cb() // Ignore errors in the pagehandler get.
|
|
405
|
+
}
|
|
406
|
+
let pageLinksMap
|
|
407
|
+
try {
|
|
408
|
+
pageLinksMap = page.story.reduce(extractPageLinks, new Map())
|
|
409
|
+
} catch (err) {
|
|
410
|
+
console.log('METADATA *** #{wikiName} reduce to extract links on #{file} failed', err.message)
|
|
411
|
+
pageLinksMap = []
|
|
412
|
+
}
|
|
413
|
+
//
|
|
414
|
+
const pageLinks = pageLinksMap.size > 0 ? Object.fromEntries(pageLinksMap) : undefined
|
|
415
|
+
|
|
416
|
+
cb(null, {
|
|
417
|
+
slug: file,
|
|
418
|
+
title: page.title,
|
|
419
|
+
date: editDate(page.journal),
|
|
420
|
+
synopsis: synopsis(page),
|
|
421
|
+
links: pageLinks,
|
|
422
|
+
})
|
|
423
|
+
})
|
|
424
|
+
}
|
|
425
|
+
async.map(files, doSitemap, (e, sitemap) => {
|
|
426
|
+
if (e) return cb(e)
|
|
427
|
+
cb(
|
|
428
|
+
null,
|
|
429
|
+
sitemap.filter(item => item != null),
|
|
430
|
+
)
|
|
431
|
+
})
|
|
432
|
+
})
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
itself.slugs = cb => {
|
|
436
|
+
fs.readdir(argv.db, { withFileTypes: true }, (e, files) => {
|
|
437
|
+
if (e) {
|
|
438
|
+
console.log('Problem reading pages directory', e)
|
|
439
|
+
return cb(e)
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
const onlyFiles = files.map(i => (i.isFile() ? i.name : null)).filter(i => i != null && !i?.startsWith('.'))
|
|
443
|
+
cb(null, onlyFiles)
|
|
444
|
+
})
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
return itself
|
|
448
|
+
}
|
package/lib/plugins.js
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Federated Wiki : Node Server
|
|
3
|
+
*
|
|
4
|
+
* Copyright Ward Cunningham and other contributors
|
|
5
|
+
* Licensed under the MIT license.
|
|
6
|
+
* https://github.com/fedwiki/wiki-server/blob/master/LICENSE.txt
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
// support server-side plugins
|
|
10
|
+
|
|
11
|
+
const fs = require('node:fs')
|
|
12
|
+
const glob = require('glob')
|
|
13
|
+
const { pathToFileURL } = require('node:url')
|
|
14
|
+
// forward = require './forward'
|
|
15
|
+
|
|
16
|
+
module.exports = exports = argv => {
|
|
17
|
+
// NOTE: plugins are now in their own package directories alongside this one...
|
|
18
|
+
// Plugins are in directories of the form wiki-package-*
|
|
19
|
+
// those with a server component will have a server directory
|
|
20
|
+
|
|
21
|
+
const plugins = {}
|
|
22
|
+
|
|
23
|
+
// http://stackoverflow.com/questions/10914751/loading-node-js-modules-dynamically-based-on-route
|
|
24
|
+
|
|
25
|
+
const startServer = (params, plugin) => {
|
|
26
|
+
const server = `${argv.packageDir}/${plugin}/server/server.js`
|
|
27
|
+
fs.exists(server, exists => {
|
|
28
|
+
if (exists) {
|
|
29
|
+
console.log('starting plugin', plugin)
|
|
30
|
+
import(pathToFileURL(server))
|
|
31
|
+
.then(exported => {
|
|
32
|
+
plugins[plugin] = exported
|
|
33
|
+
plugins[plugin].startServer?.(params)
|
|
34
|
+
})
|
|
35
|
+
.catch(e => {
|
|
36
|
+
console.log('failed to start plugin', plugin, e?.stack || e)
|
|
37
|
+
})
|
|
38
|
+
}
|
|
39
|
+
})
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const startServers = params => {
|
|
43
|
+
// emitter = new events.EventEmitter()
|
|
44
|
+
// forward.init params.app, emitter
|
|
45
|
+
// params.emitter = emitter
|
|
46
|
+
glob('wiki-plugin-*', { cwd: argv.packageDir }, (e, plugins) => {
|
|
47
|
+
plugins.forEach(plugin => {
|
|
48
|
+
startServer(params, plugin)
|
|
49
|
+
})
|
|
50
|
+
})
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
return { startServers }
|
|
54
|
+
}
|
package/lib/random_id.js
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Federated Wiki : Node Server
|
|
3
|
+
*
|
|
4
|
+
* Copyright Ward Cunningham and other contributors
|
|
5
|
+
* Licensed under the MIT license.
|
|
6
|
+
* https://github.com/fedwiki/wiki-server/blob/master/LICENSE.txt
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
// **random_id.coffee**
|
|
10
|
+
// Simple random hex generator, takes an optional number of
|
|
11
|
+
// chars that defaults to 16 and returns a random id.
|
|
12
|
+
|
|
13
|
+
const random_id = (chars = 16) => [...Array(chars)].map(() => Math.floor(Math.random() * 16).toString(16)).join('')
|
|
14
|
+
|
|
15
|
+
module.exports = random_id.random_id = random_id
|