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/lib/search.js ADDED
@@ -0,0 +1,390 @@
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
+ // **search.js**
10
+
11
+ const fs = require('node:fs')
12
+ const path = require('node:path')
13
+ const events = require('node:events')
14
+ const url = require('node:url')
15
+ const writeFileAtomic = require('write-file-atomic')
16
+
17
+ const miniSearch = require('minisearch')
18
+
19
+ module.exports = exports = argv => {
20
+ const wikiName = new URL(argv.url).hostname
21
+ let siteIndex = []
22
+ const queue = []
23
+
24
+ let searchPageHandler = null
25
+
26
+ // ms since last update we will remove index from memory
27
+ // orig - searchTimeoutMs = 1200000
28
+ const searchTimeoutMs = 120000 // temp reduce to 2 minutes
29
+ let searchTimeoutHandler = null
30
+
31
+ const siteIndexLoc = path.join(argv.status, 'site-index.json')
32
+ const indexUpdateFlag = path.join(argv.status, 'index-updated')
33
+
34
+ let working = false
35
+
36
+ const touch = (file, cb) => {
37
+ fs.stat(file, (err, stats) => {
38
+ if (err === null) return cb()
39
+ fs.open(file, 'w', (err, fd) => {
40
+ if (err) cb(err)
41
+ fs.close(fd, err => {
42
+ cb(err)
43
+ })
44
+ })
45
+ })
46
+ }
47
+
48
+ const searchPageUpdate = (slug, page, cb) => {
49
+ // to update we have to remove the page first, and then readd it
50
+ let pageText
51
+ try {
52
+ pageText = page.story.reduce(extractPageText, '')
53
+ } catch (err) {
54
+ console.log(`SITE INDEX *** ${wikiName} reduce to extract the text on ${slug} failed`, err.message)
55
+ pageText = ''
56
+ }
57
+ if (siteIndex.has(slug)) {
58
+ siteIndex.replace({
59
+ id: slug,
60
+ title: page.title,
61
+ content: pageText,
62
+ })
63
+ } else {
64
+ siteIndex.add({
65
+ id: slug,
66
+ title: page.title,
67
+ content: pageText,
68
+ })
69
+ }
70
+ cb()
71
+ }
72
+
73
+ const searchPageRemove = (slug, cb) => {
74
+ // remove page from index
75
+ try {
76
+ siteIndex.discard(slug)
77
+ } catch (err) {
78
+ // swallow error, if the page was not in index
79
+ if (!err.message.includes('not in the index')) {
80
+ console.log(`removing ${slug} from index ${wikiName} failed`, err)
81
+ }
82
+ }
83
+ cb()
84
+ }
85
+
86
+ const searchSave = (siteIndex, cb) => {
87
+ // save index to file
88
+ fs.exists(argv.status, exists => {
89
+ if (exists) {
90
+ writeFileAtomic(siteIndexLoc, JSON.stringify(siteIndex), e => {
91
+ if (e) return cb(e)
92
+ touch(indexUpdateFlag, () => {
93
+ cb()
94
+ })
95
+ })
96
+ } else {
97
+ fs.mkdir(argv.status, { recursive: true }, () => {
98
+ writeFileAtomic(siteIndexLoc, JSON.stringify(siteIndex), e => {
99
+ if (e) return cb(e)
100
+ touch(indexUpdateFlag, () => {
101
+ cb()
102
+ })
103
+ })
104
+ })
105
+ }
106
+ })
107
+ }
108
+
109
+ const searchRestore = cb => {
110
+ // restore index, or create if it doesn't already exist
111
+ fs.exists(siteIndexLoc, exists => {
112
+ if (exists) {
113
+ fs.readFile(siteIndexLoc, (err, data) => {
114
+ if (err) return cb(err)
115
+ try {
116
+ siteIndex = miniSearch.loadJSON(data, {
117
+ fields: ['title', 'content'],
118
+ })
119
+ } catch (e) {
120
+ return cb(e)
121
+ }
122
+ process.nextTick(() => {
123
+ serial(queue.shift())
124
+ })
125
+ })
126
+ }
127
+ })
128
+ }
129
+
130
+ const serial = item => {
131
+ if (item) {
132
+ switch (item.action) {
133
+ case 'update':
134
+ itself.start()
135
+ searchPageUpdate(item.slug, item.page, () => {
136
+ process.nextTick(() => {
137
+ serial(queue.shift())
138
+ })
139
+ })
140
+ break
141
+ case 'remove':
142
+ itself.start()
143
+ searchPageRemove(item.slug, () => {
144
+ process.nextTick(() => {
145
+ serial(queue.shift())
146
+ })
147
+ })
148
+ break
149
+ default:
150
+ console.log(`SITE INDEX *** unexpected action ${item.action} for ${item.page}`)
151
+ process.nextTick(() => {
152
+ serial(queue.shift)
153
+ })
154
+ }
155
+ } else {
156
+ searchSave(siteIndex, e => {
157
+ if (e) console.log('SITE INDEX *** save failed: ' + e)
158
+ itself.stop()
159
+ })
160
+ }
161
+ }
162
+
163
+ const extractItemText = text => {
164
+ return text
165
+ .replace(/\[([^\]]*?)\][[(].*?[\])]/g, ' $1 ')
166
+ .replace(/\[{2}|\[(?:[\S]+)|\]{1,2}/g, ' ')
167
+ .replace(/\n/g, ' ')
168
+ .replace(/<style.*?<\/style>/g, ' ')
169
+ .replace(/<(?:"[^"]*"['"]*|'[^']*'['"]*|[^'">])+>/g, ' ')
170
+ .replace(/<(?:[^>])+>/g, ' ')
171
+ .replace(/(https?:.*?)(?=\p{White_Space}|\p{Quotation_Mark}|$)/gu, match => {
172
+ const myUrl = url.parse(match)
173
+ return myUrl.hostname + ' ' + myUrl.pathname
174
+ })
175
+ .replace(/[\p{P}\p{Emoji}\p{Symbol}}]+/gu, ' ')
176
+ .replace(/[\p{White_Space}\n\t]+/gu, ' ')
177
+ }
178
+
179
+ const extractPageText = (pageText, currentItem, currentIndex, array) => {
180
+ // console.log('extractPageText', pageText, currentItem, currentIndex, array)
181
+ try {
182
+ if (currentItem.text) {
183
+ switch (currentItem.type) {
184
+ case 'paragraph':
185
+ case 'markdown':
186
+ case 'html':
187
+ case 'reference':
188
+ case 'image':
189
+ case 'pagefold':
190
+ case 'math':
191
+ case 'mathjax':
192
+ case 'code':
193
+ pageText += ' ' + extractItemText(currentItem.text)
194
+ break
195
+ case 'audio':
196
+ case 'video':
197
+ case 'frame':
198
+ pageText +=
199
+ ' ' +
200
+ extractItemText(
201
+ currentItem.text
202
+ .split(/\r\n?|\n/)
203
+ .map(line => {
204
+ const firstWord = line.split(/\p{White_Space}/u)[0]
205
+ if (
206
+ firstWord.startsWith('http') ||
207
+ firstWord.toUpperCase() === firstWord ||
208
+ firstWord.startsWith('//')
209
+ ) {
210
+ // line is markup
211
+ return ''
212
+ } else {
213
+ return line
214
+ }
215
+ })
216
+ .join(' '),
217
+ )
218
+ }
219
+ }
220
+ } catch (err) {
221
+ throw new Error(`Error extracting text from ${currentIndex}, ${JSON.stringify(currentItem)} ${err}, ${err.stack}`)
222
+ }
223
+ return pageText
224
+ }
225
+
226
+ // #### Public stuff ####
227
+
228
+ var itself = new events.EventEmitter()
229
+ itself.start = () => {
230
+ clearTimeout(searchTimeoutHandler)
231
+ working = true
232
+ return itself.emit('indexing')
233
+ }
234
+ itself.stop = () => {
235
+ const clearsearch = () => {
236
+ console.log(`SITE INDEX ${wikiName} : removed from memory`)
237
+ siteIndex = []
238
+ clearTimeout(searchTimeoutHandler)
239
+ }
240
+ searchTimeoutHandler = setTimeout(clearsearch, searchTimeoutMs)
241
+ working = false
242
+ return itself.emit('indexed')
243
+ }
244
+ itself.isWorking = () => {
245
+ return working
246
+ }
247
+ itself.createIndex = pagehandler => {
248
+ itself.start()
249
+
250
+ // we save the pagehandler, so we can recreate the site index if it is removed
251
+ searchPageHandler = searchPageHandler ?? pagehandler
252
+
253
+ //timeLabel = "SITE INDEX #{wikiName} : Created"
254
+ //console.time timeLabel
255
+
256
+ pagehandler.slugs((e, slugs) => {
257
+ if (e) {
258
+ console.log(`SITE INDEX *** createIndex ${wikiName} error:`, e)
259
+ itself.stop()
260
+ return e
261
+ }
262
+ siteIndex = new miniSearch({
263
+ fields: ['title', 'content'],
264
+ })
265
+
266
+ const indexPromises = slugs.map(slug => {
267
+ return new Promise(resolve => {
268
+ pagehandler.get(slug, (err, page) => {
269
+ if (err) {
270
+ console.log(`SITE INDEX *** ${wikiName}: error reading page`, slug)
271
+ return
272
+ }
273
+ // page
274
+ let pageText
275
+ try {
276
+ pageText = page.story.reduce(extractPageText, '')
277
+ } catch (err) {
278
+ console.log(`SITE INDEX *** ${wikiName} reduce to extract text on ${slug} failed`, err.message)
279
+ // console.log "page", page
280
+ pageText = ''
281
+ }
282
+ siteIndex.add({
283
+ id: slug,
284
+ title: page.title,
285
+ content: pageText,
286
+ })
287
+ resolve()
288
+ })
289
+ })
290
+ })
291
+ Promise.all(indexPromises).then(() => {
292
+ // console.timeEnd timeLabel
293
+ process.nextTick(() => {
294
+ serial(queue.shift())
295
+ })
296
+ })
297
+ })
298
+ }
299
+
300
+ itself.removePage = slug => {
301
+ const action = 'remove'
302
+ queue.push({ action, slug })
303
+ if (Array.isArray(siteIndex) && !working) {
304
+ itself.start()
305
+ searchRestore(e => {
306
+ if (e) console.log(`SITE INDEX *** Problems restoring search index ${wikiName}:` + e)
307
+ itself.createIndex(searchPageHandler)
308
+ })
309
+ } else {
310
+ if (!working) serial(queue.shift())
311
+ }
312
+ }
313
+
314
+ itself.update = (slug, page) => {
315
+ const action = 'update'
316
+ queue.push({ action, slug, page })
317
+ if (Array.isArray(siteIndex) && !working) {
318
+ itself.start()
319
+ searchRestore(e => {
320
+ if (e) console.log(`SITE INDEX *** Problems restoring search index ${wikiName}:` + e)
321
+ itself.createIndex(searchPageHandler)
322
+ })
323
+ } else {
324
+ if (!working) serial(queue.shift())
325
+ }
326
+ }
327
+ itself.startUp = pagehandler => {
328
+ // called on server startup, here we check if wiki already is index
329
+ // we only create an index if there is either no index or there have been updates since last startup
330
+ console.log(`SITE INDEX ${wikiName} : StartUp`)
331
+ fs.stat(siteIndexLoc, (err, stats) => {
332
+ if (err === null) {
333
+ // site index exists, but has it been updated?
334
+ fs.stat(indexUpdateFlag, (err, stats) => {
335
+ if (!err) {
336
+ // index has been updated, so recreate it.
337
+ itself.createIndex(pagehandler)
338
+ // remove the update flag once the index has been created
339
+ itself.once('indexed', () => {
340
+ fs.unlink(indexUpdateFlag, err => {
341
+ if (err) console.log(`+++ SITE INDEX ${wikiName} : unable to delete update flag`)
342
+ })
343
+ })
344
+ } else {
345
+ // not been updated, but is it the correct version?
346
+ fs.readFile(siteIndexLoc, (err, data) => {
347
+ if (!err) {
348
+ let testIndex
349
+ try {
350
+ testIndex = JSON.parse(data)
351
+ } catch (err) {
352
+ testIndex = {}
353
+ }
354
+ if (testIndex.serializationVersion != 2)
355
+ console.log(`+++ SITE INDEX ${wikiName} : updating to latest version.`)
356
+ itself.createIndex(pagehandler)
357
+ // remove the update flag once the index has been created
358
+ itself.once('indexed', () => {
359
+ fs.unlink(indexUpdateFlag, err => {
360
+ if (err) console.log(`+++ SITE INDEX ${wikiName} : unable to delete update flag`)
361
+ })
362
+ })
363
+ } else {
364
+ console.log(`+++ SITE INDEX ${wikiName} : error reading index - attempting creating`)
365
+ itself.createIndex(pagehandler)
366
+ // remove the update flag once the index has been created
367
+ itself.once('indexed', () => {
368
+ fs.unlink(indexUpdateFlag, err => {
369
+ if (err) console.log(`+++ SITE INDEX ${wikiName} : unable to delete update flag`)
370
+ })
371
+ })
372
+ }
373
+ })
374
+ }
375
+ })
376
+ } else {
377
+ // index does not exist, so create it
378
+ itself.createIndex(pagehandler)
379
+ // remove the update flag once the index has been created
380
+ itself.once('indexed', () => {
381
+ fs.unlink(indexUpdateFlag, err => {
382
+ if (err) console.log(`+++ SITE INDEX ${wikiName} : unable to delete update flag`)
383
+ })
384
+ })
385
+ }
386
+ })
387
+ }
388
+
389
+ return itself
390
+ }
@@ -0,0 +1,94 @@
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-node-server/blob/master/LICENSE.txt
7
+ */
8
+ // **security.js**
9
+ // Module for default site security.
10
+ //
11
+ // This module is not intented for use, but is here to catch a problem with
12
+ // configuration of security. It does not provide any authentication, but will
13
+ // allow the server to run read-only.
14
+
15
+ // #### Requires ####
16
+ const fs = require('node:fs')
17
+
18
+ // Export a function that generates security handler
19
+ // when called with options object.
20
+ module.exports = exports = (log, loga, argv) => {
21
+ const security = {}
22
+
23
+ // #### Private utility methods. ####
24
+
25
+ const user = ''
26
+
27
+ let owner = ''
28
+
29
+ // save the admin user, and location of the identity file
30
+ const { admin, id: idFile } = argv
31
+
32
+ // #### Public stuff ####
33
+
34
+ security.authenticate_session = () => {
35
+ ;(req, res, next) => {
36
+ // not possible to login, so always false
37
+ req.isAuthenticated = () => false
38
+ return next()
39
+ }
40
+ }
41
+
42
+ // Retrieve owner infomation from identity file in status directory
43
+ security.retrieveOwner = cb => {
44
+ fs.exists(idFile, exists => {
45
+ if (exists) {
46
+ fs.readFile(idFile, (err, data) => {
47
+ if (err) return cb(err)
48
+ owner += data
49
+ cb()
50
+ })
51
+ } else {
52
+ owner = ''
53
+ cb()
54
+ }
55
+ })
56
+ }
57
+
58
+ // Return the owners name
59
+ security.getOwner = () => {
60
+ let ownerName
61
+ if (!owner.name) {
62
+ ownerName = ''
63
+ } else {
64
+ ownerName = owner.name
65
+ }
66
+ return ownerName
67
+ }
68
+ security.getUser = req => {
69
+ return ''
70
+ }
71
+
72
+ security.isAuthorized = req => {
73
+ // nobody is authorized - everything is read-only
74
+ // unless legacy support, when unclaimed sites can be editted.
75
+ if (owner == '') {
76
+ if (argv.security_legacy) {
77
+ return true
78
+ } else {
79
+ return false
80
+ }
81
+ } else {
82
+ return false
83
+ }
84
+ }
85
+ // Wiki server admin
86
+ security.isAdmin = () => {
87
+ return false
88
+ }
89
+ security.defineRoutes = (app, cors, updateOwner) => {
90
+ // default security does not have any routes
91
+ }
92
+
93
+ return security
94
+ }