wiki-server 0.25.9 → 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/sitemap.js ADDED
@@ -0,0 +1,288 @@
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
+ // **sitemap.coffee**
10
+
11
+ const fs = require('fs')
12
+ const path = require('path')
13
+ const events = require('events')
14
+ const writeFileAtomic = require('write-file-atomic')
15
+ const xml2js = require('xml2js')
16
+
17
+ const synopsis = require('wiki-client/lib/synopsis')
18
+
19
+ const asSlug = name =>
20
+ name
21
+ .replace(/\s/g, '-')
22
+ .replace(/[^A-Za-z0-9-]/g, '')
23
+ .toLowerCase()
24
+
25
+ module.exports = exports = argv => {
26
+ const wikiName = new URL(argv.url).hostname
27
+
28
+ let sitemap = []
29
+
30
+ const queue = []
31
+
32
+ let sitemapPageHandler = null
33
+
34
+ // ms since last update we will remove sitemap from memory
35
+ const sitemapTimeoutMs = 120000
36
+ let sitemapTimeoutHandler = null
37
+
38
+ const sitemapLoc = path.join(argv.status, 'sitemap.json')
39
+ const xmlSitemapLoc = path.join(argv.status, 'sitemap.xml')
40
+
41
+ let working = false
42
+
43
+ const lastEdit = journal => {
44
+ if (!journal) return undefined
45
+ // find the last journal entry, that is not a fork, with a date.
46
+ const last = journal.findLast(action => {
47
+ return action.date && action.type != 'fork'
48
+ })
49
+ return last ? last.date : undefined
50
+ }
51
+
52
+ const sitemapUpdate = (file, page, cb) => {
53
+ let pageLinks, pageLinksMap
54
+ const extractPageLinks = (collaborativeLinks, currentItem, currentIndex, array) => {
55
+ // extract collaborative links
56
+ // - this will need extending if we also extract the id of the item containing the link
57
+ try {
58
+ const linkRe = /\[\[([^\]]+)\]\]/g
59
+ let match = undefined
60
+ while ((match = linkRe.exec(currentItem.text)) != null) {
61
+ if (!collaborativeLinks.has(asSlug(match[1]))) {
62
+ collaborativeLinks.set(asSlug(match[1]), currentItem.id)
63
+ }
64
+ }
65
+ if ('reference' == currentItem.type) {
66
+ if (!collaborativeLinks.has(currentItem.slug)) {
67
+ collaborativeLinks.set(currentItem.slug, currentItem.id)
68
+ }
69
+ }
70
+ } catch (err) {
71
+ console.log(
72
+ `METADATA *** ${wikiName} Error extracting links from ${currentIndex} of ${JSON.stringify(array)}`,
73
+ err.message,
74
+ )
75
+ }
76
+ return collaborativeLinks
77
+ }
78
+ try {
79
+ pageLinksMap = page.story.reduce(extractPageLinks, new Map())
80
+ } catch (err) {
81
+ console.log(`METADATA *** ${wikiName} reduce to extract links on ${file} failed`, err.message)
82
+ pageLinksMap = []
83
+ }
84
+ //
85
+ if (pageLinksMap.size > 0) {
86
+ pageLinks = Object.fromEntries(pageLinksMap)
87
+ } else {
88
+ pageLinks = undefined
89
+ }
90
+
91
+ const entry = {
92
+ slug: file,
93
+ title: page.title,
94
+ date: lastEdit(page.journal),
95
+ synopsis: synopsis(page),
96
+ links: pageLinks,
97
+ }
98
+
99
+ const slugs = sitemap.map(page => page.slug)
100
+
101
+ const idx = slugs.indexOf(file)
102
+
103
+ if (~idx) {
104
+ sitemap[idx] = entry
105
+ } else {
106
+ sitemap.push(entry)
107
+ }
108
+ cb()
109
+ }
110
+
111
+ const sitemapRemovePage = (file, cb) => {
112
+ const slugs = sitemap.map(page => page.slug)
113
+ const idx = slugs.indexOf(file)
114
+
115
+ if (~idx) {
116
+ sitemap.splice(idx, 1)
117
+ }
118
+ cb()
119
+ }
120
+
121
+ const sitemapSave = (sitemap, cb) => {
122
+ fs.exists(argv.status, exists => {
123
+ if (exists) {
124
+ writeFileAtomic(sitemapLoc, JSON.stringify(sitemap), e => {
125
+ if (e) return cb(e)
126
+ cb()
127
+ })
128
+ } else
129
+ fs.mkdir(argv.status, { recursive: true }, () => {
130
+ writeFileAtomic(sitemapLoc, JSON.stringify(sitemap), e => {
131
+ if (e) return cb(e)
132
+ cb()
133
+ })
134
+ })
135
+ })
136
+ }
137
+
138
+ const sitemapRestore = cb => {
139
+ fs.exists(sitemapLoc, exists => {
140
+ if (exists) {
141
+ fs.readFile(sitemapLoc, (err, data) => {
142
+ if (err) return cb(err)
143
+ try {
144
+ sitemap = JSON.parse(data)
145
+ } catch (e) {
146
+ return cb(e)
147
+ }
148
+ process.nextTick(() => {
149
+ serial(queue.shift())
150
+ })
151
+ })
152
+ } else {
153
+ // sitemap file does not exist, so needs creating
154
+ itself.createSitemap(sitemapPageHandler)
155
+ }
156
+ })
157
+ }
158
+
159
+ const xmlSitemapSave = (sitemap, cb) => {
160
+ const xmlmapPages = []
161
+ sitemap.forEach(page => {
162
+ const result = {}
163
+ result['loc'] = argv.url + '/' + page.slug + '.html'
164
+ if (page.date) {
165
+ const date = new Date(page.date)
166
+ if (!isNaN(date.valueOf())) {
167
+ result['lastmod'] = date.toISOString().substring(0, 10)
168
+ }
169
+ }
170
+ xmlmapPages.push(result)
171
+ })
172
+ const xmlmap = { urlset: { $: { xmlns: 'http://www.sitemaps.org/schemas/sitemap/0.9' }, url: xmlmapPages } }
173
+ const builder = new xml2js.Builder()
174
+ const xml = builder.buildObject(xmlmap)
175
+ fs.exists(argv.status, exists => {
176
+ if (exists) {
177
+ writeFileAtomic(xmlSitemapLoc, xml, e => {
178
+ if (e) return cb(e)
179
+ cb()
180
+ })
181
+ } else {
182
+ fs.mkdir(argv.status, { recursive: true }, () => {
183
+ writeFileAtomic(xmlSitemapLoc, xml, e => {
184
+ if (e) return cb(e)
185
+ cb()
186
+ })
187
+ })
188
+ }
189
+ })
190
+ }
191
+
192
+ const serial = item => {
193
+ if (item) {
194
+ switch (item.action) {
195
+ case 'update':
196
+ itself.start()
197
+ sitemapUpdate(item.file, item.page, e => process.nextTick(() => serial(queue.shift())))
198
+ break
199
+ case 'remove':
200
+ itself.start()
201
+ sitemapRemovePage(item.file, e => process.nextTick(() => serial(queue.shift())))
202
+ break
203
+ default:
204
+ console.log(`Sitemap unexpected action ${item.action} for ${item.page} in ${wikiName}`)
205
+ process.nextTick(() => serial(queue.shift))
206
+ }
207
+ } else
208
+ sitemapSave(sitemap, e => {
209
+ if (e) console.log(`Problems saving sitemap ${wikiName}: ` + e)
210
+ itself.stop()
211
+ })
212
+ xmlSitemapSave(sitemap, e => {
213
+ if (e) console.log(`Problems saving sitemap(xml) ${wikiName}`) + e
214
+ })
215
+ }
216
+
217
+ // #### Public stuff ####
218
+
219
+ const itself = new events.EventEmitter()
220
+ itself.start = () => {
221
+ clearTimeout(sitemapTimeoutHandler)
222
+ working = true
223
+ itself.emit('working')
224
+ }
225
+ itself.stop = () => {
226
+ const clearsitemap = () => {
227
+ console.log(`removing sitemap ${wikiName} from memory`)
228
+ sitemap.length = 0
229
+ clearTimeout(sitemapTimeoutHandler)
230
+ }
231
+ // don't clear sitemap when in test environment. It just delays the tests completing.
232
+ if (!argv.test) sitemapTimeoutHandler = setTimeout(clearsitemap, sitemapTimeoutMs)
233
+ working = false
234
+ itself.emit('finished')
235
+ }
236
+ itself.isWorking = () => {
237
+ working
238
+ }
239
+
240
+ itself.createSitemap = pagehandler => {
241
+ itself.start()
242
+ // we save the pagehandler, so we can recreate the sitemap if it is removed
243
+ if (!sitemapPageHandler) sitemapPageHandler = pagehandler
244
+
245
+ pagehandler.pages((e, newsitemap) => {
246
+ if (e) {
247
+ console.log(`createSitemap ${wikiName} : error ` + e)
248
+ itself.stop()
249
+ return e
250
+ }
251
+ sitemap = newsitemap
252
+
253
+ process.nextTick(() => {
254
+ serial(queue.shift())
255
+ })
256
+ })
257
+ }
258
+
259
+ itself.removePage = file => {
260
+ const action = 'remove'
261
+ queue.push({ action, file })
262
+ if (sitemap.length === 0 && !working) {
263
+ itself.start()
264
+ sitemapRestore(e => {
265
+ if (e) console.log(`Problems restoring sitemap ${wikiName} : ` + e)
266
+ itself.createSitemap(sitemapPageHandler)
267
+ })
268
+ } else {
269
+ if (!working) serial(queue.shift())
270
+ }
271
+ }
272
+
273
+ itself.update = (file, page) => {
274
+ const action = 'update'
275
+ queue.push({ action, file, page })
276
+ if (sitemap.length === 0 && !working) {
277
+ itself.start()
278
+ sitemapRestore(e => {
279
+ if (e) console.log(`Problems restoring sitemap ${wikiName} : ` + e)
280
+ itself.createSitemap(sitemapPageHandler)
281
+ })
282
+ } else {
283
+ if (!working) serial(queue.shift())
284
+ }
285
+ }
286
+
287
+ return itself
288
+ }
package/package.json CHANGED
@@ -1,23 +1,33 @@
1
1
  {
2
2
  "name": "wiki-server",
3
3
  "description": "A Federated Wiki Server",
4
- "version": "0.25.9",
4
+ "version": "0.26.0-rc.0",
5
5
  "author": {
6
6
  "name": "Ward Cunningham",
7
7
  "email": "ward@c2.com",
8
8
  "url": "http://ward.fed.wiki.org"
9
9
  },
10
10
  "contributors": [
11
- {
12
- "name": "Nick Niemeir",
13
- "email": "nick.niemeir@gmail.com",
14
- "url": "http://nrn.io"
15
- },
16
- {
17
- "name": "Paul Rodwell",
18
- "email": "paul.rodwell@btinternet.com",
19
- "url": "http://wiki-paul90.rhcloud.com"
20
- }
11
+ "Austin King <shout@ozten.com>",
12
+ "Paul Rodwell <paul.rodwell@btinternet.com>",
13
+ "Nicholas Hallahan <nick@theoutpost.io>",
14
+ "Tom Lee <github@tomlee.co>",
15
+ "Erkan Yilmaz <erkan77@gmail.com>",
16
+ "Patrick Mueller <pmuellr@apache.org>",
17
+ "Nick Niemeir <nick.niemeir@gmail.com>",
18
+ "Ward Cunningham <ward@c2.com>",
19
+ "Christian Smith <smith@anvil.io>",
20
+ "Gui13 <guitreize@gmail.com>",
21
+ "Merlyn Albery-Speyer <merlyn@newrelic.com>",
22
+ "Marcin Cieslak <saper@saper.info>",
23
+ "enyst <engel.nyst@gmail.com>",
24
+ "Peter deHaan <pdehaan@users.noreply.github.com>",
25
+ "winckell benjamin <winckell.benjamin.isart@gmail.com>",
26
+ "Eric Dobbs <eric@dobbse.net>",
27
+ "Joshua Benuck <joshua_benuck@yahoo.com>",
28
+ "Tom Lieber <tom@alltom.com>",
29
+ "Andrew Shell <andrew@andrewshell.org>",
30
+ "jon r <jon@allmende.io>"
21
31
  ],
22
32
  "dependencies": {
23
33
  "async": "^3.2.4",
@@ -27,7 +37,7 @@
27
37
  "cookie-parser": "^1.4.4",
28
38
  "dompurify": "^3.1.0",
29
39
  "errorhandler": "^1.5.1",
30
- "express": "^4.17.1",
40
+ "express": "^4.21.2",
31
41
  "express-hbs": "^2.5.0",
32
42
  "flates": "0.0.5",
33
43
  "glob": "^7.2.3",
@@ -40,25 +50,24 @@
40
50
  "xml2js": "^0.6.2"
41
51
  },
42
52
  "scripts": {
43
- "test": "grunt mochaTest"
53
+ "prettier:format": "prettier --write './**/*.js'",
54
+ "prettier:check": "prettier --check ./**/*.js",
55
+ "test": "node --test",
56
+ "update-authors": "node scripts/update-authors.js"
44
57
  },
45
58
  "devDependencies": {
46
- "grunt": "^1.6.1",
47
- "grunt-contrib-watch": "^1.1.0",
59
+ "@eslint/js": "^9.29.0",
60
+ "eslint": "^9.29.0",
61
+ "globals": "^16.1.0",
48
62
  "grunt-git-authors": "^3.2.0",
49
- "grunt-mocha-test": "^0.13.3",
50
- "mocha": "^11.1.0",
51
- "should": "^13.2.3",
63
+ "prettier": "^3.5.3",
52
64
  "supertest": "^7.0.0",
53
- "wiki-client": "^0.31",
65
+ "wiki-client": "^0.31.2",
54
66
  "wiki-plugin-activity": "0.7",
55
67
  "wiki-plugin-video": "^0.4"
56
68
  },
57
69
  "engines": {
58
- "node": ">=18.x"
59
- },
60
- "testling": {
61
- "harness": "mocha"
70
+ "node": ">=20.x"
62
71
  },
63
72
  "license": "MIT",
64
73
  "repository": {
@@ -1,31 +0,0 @@
1
- # This workflow will do a clean installation of node dependencies, cache/restore them, build the source code and run tests across different versions of node
2
- # For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-nodejs
3
-
4
- name: Node.js CI
5
-
6
- on:
7
- push:
8
- branches: [ "master" ]
9
- pull_request:
10
- branches: [ "master" ]
11
-
12
- jobs:
13
- build:
14
-
15
- runs-on: ubuntu-latest
16
-
17
- strategy:
18
- matrix:
19
- node-version: [18.x, 20.x, 22.x]
20
- # See supported Node.js release schedule at https://nodejs.org/en/about/releases/
21
-
22
- steps:
23
- - uses: actions/checkout@v4
24
- - name: Use Node.js ${{ matrix.node-version }}
25
- uses: actions/setup-node@v4
26
- with:
27
- node-version: ${{ matrix.node-version }}
28
- cache: 'npm'
29
- - run: npm ci
30
- - run: npm run build --if-present
31
- - run: npm test
package/.mailmap DELETED
@@ -1 +0,0 @@
1
- Nick Niemeir <nick.niemeir@gmail.com> <nniemeir@newrelic.com>
package/Gruntfile.js DELETED
@@ -1,55 +0,0 @@
1
- module.exports = function( grunt ) {
2
-
3
- "use strict";
4
-
5
- grunt.loadNpmTasks('grunt-contrib-watch');
6
- grunt.loadNpmTasks('grunt-mocha-test');
7
- grunt.loadNpmTasks('grunt-git-authors');
8
-
9
- grunt.initConfig({
10
- pkg: grunt.file.readJSON('package.json'),
11
-
12
- mochaTest: {
13
- test: {
14
- options: {
15
- reporter: 'spec',
16
- require: [
17
- 'coffeescript/register',
18
- 'should'
19
- ]
20
- },
21
- src: [
22
- 'test/defaultargs.coffee',
23
- 'test/page.coffee',
24
- 'test/random.coffee',
25
- 'test/server.coffee',
26
- 'test/sitemap.coffee'
27
- ]
28
- }
29
- },
30
-
31
- authors: {
32
- prior: [
33
- "Ward Cunningham <ward@c2.com>",
34
- "Nick Niemeir <nick.niemeir@gmail.com>",
35
- "Patrick Mueller <pmuellr@apache.org>",
36
- "Erkan Yilmaz <erkan77@gmail.com>",
37
- "Tom Lee <github@tomlee.co>",
38
- "Nicholas Hallahan <nick@theoutpost.io>",
39
- "Paul Rodwell <paul.rodwell@btinternet.com>",
40
- "Austin King <shout@ozten.com>"
41
- ]
42
- },
43
-
44
- watch: {
45
- all: {
46
- files: ['lib/*.coffee', 'test/*.coffee'],
47
- tasks: ['mochaTest']
48
- }
49
- }
50
- });
51
-
52
- grunt.registerTask('default', ['mochaTest']);
53
- grunt.registerTask('check', ['retire']);
54
-
55
- }
@@ -1,77 +0,0 @@
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
-
10
- # **defaultargs.coffee** when called on the argv object this
11
- # module will create reasonable defaults for options not supplied,
12
- # based on what information is provided.
13
- path = require 'path'
14
-
15
- getUserHome = ->
16
- process.env.HOME or process.env.HOMEPATH or process.env.USERPROFILE
17
-
18
- module.exports = (argv) ->
19
- argv or= {}
20
- argv.root or= __dirname
21
- # the directory that contains all the packages that makeup the wiki
22
- argv.packageDir or= path.join(argv.root, "..")
23
- argv.port or= 3000
24
- argv.home or= 'welcome-visitors'
25
- argv.data or= path.join(getUserHome(), '.wiki') # see also cli
26
- argv.client or= path.join(argv.packageDir, 'wiki-client', 'client')
27
- argv.db or= path.join(argv.data, 'pages')
28
- argv.status or= path.join(argv.data, 'status')
29
- argv.assets or= path.join(argv.data, 'assets')
30
- argv.recycler or= path.join(argv.data, 'recycle')
31
- argv.commons or= path.join(argv.data, 'commons')
32
- argv.url or= 'http://localhost' + (if argv.port is 80 then '' else ':' + argv.port)
33
- argv.id or= path.join(argv.status, 'owner.json')
34
- argv.uploadLimit or= '5mb'
35
- argv.cookieSecret or= require('crypto').randomBytes(64).toString('hex')
36
- argv.secure_cookie or= false
37
- argv.session_duration or= 7
38
- argv.neighbors or= ''
39
- argv.debug or= false
40
-
41
- if typeof(argv.database) is 'string'
42
- argv.database = JSON.parse(argv.database)
43
- argv.database or= {}
44
- argv.database.type or= './page'
45
- if argv.database.type.charAt(0) is '.'
46
- if argv.database.type != './page'
47
- console.log "\n\nWARNING: This storage option is depeciated."
48
- console.log " See ReadMe for details of the changes required.\n\n"
49
- else
50
- argv.database.type = 'wiki-storage-' + argv.database.type
51
-
52
- argv.security_type or= './security'
53
- if argv.security_type is './security'
54
- console.log "\n\nINFORMATION: Using default security module."
55
- else
56
- argv.security_type = 'wiki-security-' + argv.security_type
57
- argv.security_legacy or= false
58
-
59
- #resolve all relative paths
60
- argv.root = path.resolve(argv.root)
61
- argv.packageDir = path.resolve(argv.packageDir)
62
- argv.data = path.resolve(argv.data)
63
- argv.client = path.resolve(argv.client)
64
- argv.db = path.resolve(argv.db)
65
- argv.status = path.resolve(argv.status)
66
- argv.assets = path.resolve(argv.assets)
67
- argv.recycler = path.resolve(argv.recycler)
68
- argv.commons = path.resolve(argv.commons)
69
- argv.id = path.resolve(argv.id)
70
-
71
- if /node_modules/.test(argv.data)
72
- console.log "\n\nWARNING : The dafault data path is not a safe place."
73
- console.log " : by using ", argv.data, " your pages will be lost when packages are updated."
74
- console.log " : You are strongly advised to use an alternative directory."
75
- console.log " : See the wiki package ReadMe for how to do this.\n\n"
76
-
77
- argv