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/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/lib/server.coffee
DELETED
|
@@ -1,782 +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
|
-
# **server.coffee** is the main guts of the express version
|
|
10
|
-
# of (Smallest Federated Wiki)[https://github.com/WardCunningham/Smallest-Federated-Wiki].
|
|
11
|
-
# The CLI and Farm are just front ends
|
|
12
|
-
# for setting arguments, and spawning servers. In a complex system
|
|
13
|
-
# you would probably want to replace the CLI/Farm with your own code,
|
|
14
|
-
# and use server.coffee directly.
|
|
15
|
-
#
|
|
16
|
-
#### Dependencies ####
|
|
17
|
-
# anything not in the standard library is included in the repo, or
|
|
18
|
-
# can be installed with an:
|
|
19
|
-
# npm install
|
|
20
|
-
|
|
21
|
-
# Standard lib
|
|
22
|
-
fs = require 'fs'
|
|
23
|
-
path = require 'path'
|
|
24
|
-
http = require 'http'
|
|
25
|
-
url = require 'url'
|
|
26
|
-
{ pipeline } = require 'node:stream/promises'
|
|
27
|
-
|
|
28
|
-
# From npm
|
|
29
|
-
express = require 'express'
|
|
30
|
-
hbs = require 'express-hbs'
|
|
31
|
-
glob = require 'glob'
|
|
32
|
-
async = require 'async'
|
|
33
|
-
f = require('flates')
|
|
34
|
-
|
|
35
|
-
createDOMPurify = require('dompurify')
|
|
36
|
-
{ JSDOM } = require('jsdom')
|
|
37
|
-
|
|
38
|
-
window = new JSDOM('').window
|
|
39
|
-
DOMPurify = createDOMPurify(window)
|
|
40
|
-
|
|
41
|
-
# node-fetch is now ESM only
|
|
42
|
-
fetch = (...args) => import('node-fetch').then(({default: fetch}) => fetch(...args));
|
|
43
|
-
|
|
44
|
-
# Express 4 middleware
|
|
45
|
-
logger = require 'morgan'
|
|
46
|
-
cookieParser = require 'cookie-parser'
|
|
47
|
-
methodOverride = require 'method-override'
|
|
48
|
-
## session = require 'express-session'
|
|
49
|
-
sessions = require 'client-sessions'
|
|
50
|
-
bodyParser = require 'body-parser'
|
|
51
|
-
errorHandler = require 'errorhandler'
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
# Local files
|
|
56
|
-
random = require './random_id'
|
|
57
|
-
defargs = require './defaultargs'
|
|
58
|
-
resolveClient = require 'wiki-client/lib/resolve'
|
|
59
|
-
pluginsFactory = require './plugins'
|
|
60
|
-
sitemapFactory = require './sitemap'
|
|
61
|
-
searchFactory = require './search'
|
|
62
|
-
|
|
63
|
-
render = (page) ->
|
|
64
|
-
return f.div({class: "twins"}, f.p('')) + '\n' +
|
|
65
|
-
f.div({class: "header"}, f.h1(
|
|
66
|
-
f.a({href: '/', style: 'text-decoration: none'},
|
|
67
|
-
f.img({height: '32px', src: '/favicon.png'})) +
|
|
68
|
-
' ' + (page.title))) + '\n' +
|
|
69
|
-
f.div {class: "story"},
|
|
70
|
-
page.story.map((story) ->
|
|
71
|
-
return '' unless story
|
|
72
|
-
if story.type is 'paragraph'
|
|
73
|
-
f.div {class: "item paragraph"}, f.p(resolveClient.resolveLinks(story.text))
|
|
74
|
-
else if story.type is 'image'
|
|
75
|
-
f.div {class: "item image"},
|
|
76
|
-
f.img({class: "thumbnail", src: story.url}),
|
|
77
|
-
f.p(resolveClient.resolveLinks(story.text or story.caption or 'uploaded image'))
|
|
78
|
-
else if story.type is 'html'
|
|
79
|
-
f.div {class: "item html"},
|
|
80
|
-
f.p(resolveClient.resolveLinks(story.text or '', DOMPurify.sanitize))
|
|
81
|
-
else f.div {class: "item"}, f.p(resolveClient.resolveLinks(story.text or ''))
|
|
82
|
-
).join('\n')
|
|
83
|
-
|
|
84
|
-
# Set export objects for node and coffee to a function that generates a sfw server.
|
|
85
|
-
module.exports = exports = (argv) ->
|
|
86
|
-
# Create the main application object, app.
|
|
87
|
-
app = express()
|
|
88
|
-
|
|
89
|
-
# remove x-powered-by header
|
|
90
|
-
app.disable('x-powered-by')
|
|
91
|
-
|
|
92
|
-
# defaultargs.coffee exports a function that takes the argv object
|
|
93
|
-
# that is passed in and then does its
|
|
94
|
-
# best to supply sane defaults for any arguments that are missing.
|
|
95
|
-
argv = defargs(argv)
|
|
96
|
-
|
|
97
|
-
app.startOpts = argv
|
|
98
|
-
|
|
99
|
-
log = (stuff...) ->
|
|
100
|
-
console.log stuff if argv.debug
|
|
101
|
-
|
|
102
|
-
loga = (stuff...) ->
|
|
103
|
-
console.log stuff
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
ourErrorHandler = (req, res, next) ->
|
|
107
|
-
fired = false
|
|
108
|
-
res.e = (error, status) ->
|
|
109
|
-
if !fired
|
|
110
|
-
fired = true
|
|
111
|
-
res.statusCode = status or 500
|
|
112
|
-
res.end 'Server ' + error
|
|
113
|
-
log "Res sent:", res.statusCode, error
|
|
114
|
-
else
|
|
115
|
-
log "Already fired", error
|
|
116
|
-
next()
|
|
117
|
-
|
|
118
|
-
# Require the database adapter and initialize it with options.
|
|
119
|
-
app.pagehandler = pagehandler = require(argv.database.type)(argv)
|
|
120
|
-
|
|
121
|
-
# Require the sitemap adapter and initialize it with options.
|
|
122
|
-
app.sitemaphandler = sitemaphandler = sitemapFactory(argv)
|
|
123
|
-
|
|
124
|
-
# Require the site indexer and initialize it with options
|
|
125
|
-
app.searchhandler = searchhandler = searchFactory(argv)
|
|
126
|
-
|
|
127
|
-
# Require the security adapter and initialize it with options.
|
|
128
|
-
app.securityhandler = securityhandler = require(argv.security_type)(log, loga, argv)
|
|
129
|
-
|
|
130
|
-
# If the site is owned, owner will contain the name of the owner
|
|
131
|
-
owner = ''
|
|
132
|
-
|
|
133
|
-
# If the user is logged in, user will contain their identity
|
|
134
|
-
user = ''
|
|
135
|
-
|
|
136
|
-
# Called from authentication when the site is claimed,
|
|
137
|
-
# to update the name of the owner held here.
|
|
138
|
-
updateOwner = (id) ->
|
|
139
|
-
owner = id
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
#### Middleware ####
|
|
143
|
-
#
|
|
144
|
-
# Allow json to be got cross origin.
|
|
145
|
-
cors = (req, res, next) ->
|
|
146
|
-
res.header 'Access-Control-Allow-Origin', req.get('origin')||'*'
|
|
147
|
-
next()
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
remoteGet = (remote, slug, cb) ->
|
|
151
|
-
# assume http, as we know no better at this point and we need to specify a protocol.
|
|
152
|
-
remoteURL = new URL("http://#{remote}/#{slug}.json").toString()
|
|
153
|
-
# set a two second timeout
|
|
154
|
-
fetch(remoteURL, {signal: AbortSignal.timeout(2000)})
|
|
155
|
-
.then (res) ->
|
|
156
|
-
if res.ok
|
|
157
|
-
return res
|
|
158
|
-
throw new Error(res.statusText)
|
|
159
|
-
.then (res) ->
|
|
160
|
-
return res.json()
|
|
161
|
-
.then (json) ->
|
|
162
|
-
cb(null, json, 200)
|
|
163
|
-
.catch (err) ->
|
|
164
|
-
console.error('Unable to fetch remote resource', remote, slug, err)
|
|
165
|
-
cb(err, 'Page not found', 404)
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
#### Express configuration ####
|
|
170
|
-
# Set up all the standard express server options,
|
|
171
|
-
# including hbs to use handlebars/mustache templates
|
|
172
|
-
# saved with a .html extension, and no layout.
|
|
173
|
-
|
|
174
|
-
#
|
|
175
|
-
staticPathOptions = {
|
|
176
|
-
dotfiles: 'ignore'
|
|
177
|
-
etag: true
|
|
178
|
-
immutable: false
|
|
179
|
-
lastModified: false
|
|
180
|
-
maxAge: '1h'
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
app.set('views',
|
|
184
|
-
path.join(require.resolve('wiki-client/package.json'), '..', 'views'))
|
|
185
|
-
app.set('view engine', 'html')
|
|
186
|
-
app.engine('html', hbs.express4())
|
|
187
|
-
app.set('view options', layout: false)
|
|
188
|
-
|
|
189
|
-
# return deterministically colored strings
|
|
190
|
-
colorString = (str) ->
|
|
191
|
-
colorReset = '\x1b[0m'
|
|
192
|
-
hash = 0;
|
|
193
|
-
str.split('').forEach (char) ->
|
|
194
|
-
hash = char.charCodeAt(0) + ((hash << 5) - hash)
|
|
195
|
-
color = '\x1b[38;2'
|
|
196
|
-
for i in [0..2]
|
|
197
|
-
do (i) ->
|
|
198
|
-
value = (hash >> (i * 8)) & 0xff
|
|
199
|
-
color += ';' + value.toString()
|
|
200
|
-
color += 'm'
|
|
201
|
-
return color + str + colorReset
|
|
202
|
-
|
|
203
|
-
# use logger, at least in development, probably needs a param to configure (or turn off).
|
|
204
|
-
# use stream to direct to somewhere other than stdout.
|
|
205
|
-
logger.token('vhost', (req, res) ->
|
|
206
|
-
return colorString(req.hostname))
|
|
207
|
-
app.use(logger(':vhost :method :url :status :res[content-length] - :response-time ms'))
|
|
208
|
-
app.use(cookieParser())
|
|
209
|
-
app.use(bodyParser.json({ limit: argv.uploadLimit}))
|
|
210
|
-
app.use(bodyParser.urlencoded({ extended: true, limit: argv.uploadLimit}))
|
|
211
|
-
app.use(methodOverride())
|
|
212
|
-
cookieValue = {
|
|
213
|
-
httpOnly: true
|
|
214
|
-
sameSite: 'lax'
|
|
215
|
-
}
|
|
216
|
-
if argv.wiki_domain
|
|
217
|
-
if !argv.wiki_domain.endsWith('localhost')
|
|
218
|
-
cookieValue['domain'] = argv.wiki_domain
|
|
219
|
-
# use secureProxy as TLS is terminated in outside the node process
|
|
220
|
-
if argv.secure_cookie
|
|
221
|
-
cookieName = 'wikiTlsSession'
|
|
222
|
-
cookieValue['secureProxy'] = true
|
|
223
|
-
else
|
|
224
|
-
cookieName = "wikiSession"
|
|
225
|
-
app.use(sessions({
|
|
226
|
-
cookieName: cookieName,
|
|
227
|
-
requestKey: 'session',
|
|
228
|
-
secret: argv.cookieSecret,
|
|
229
|
-
# make the session session_duration days long
|
|
230
|
-
duration: argv.session_duration * 24 * 60 * 60 * 1000,
|
|
231
|
-
# add 12 hours to session if less than 12 hours to expiry
|
|
232
|
-
activeDuration: 24 * 60 * 60 * 1000,
|
|
233
|
-
cookie: cookieValue
|
|
234
|
-
}))
|
|
235
|
-
|
|
236
|
-
app.use(ourErrorHandler)
|
|
237
|
-
|
|
238
|
-
# Add static route to the client
|
|
239
|
-
app.use(express.static(argv.client, staticPathOptions))
|
|
240
|
-
|
|
241
|
-
##### Define security routes #####
|
|
242
|
-
securityhandler.defineRoutes app, cors, updateOwner
|
|
243
|
-
|
|
244
|
-
# Add static route to assets
|
|
245
|
-
app.use('/assets', cors, express.static(argv.assets))
|
|
246
|
-
|
|
247
|
-
# Add static routes to the plugins client.
|
|
248
|
-
glob "wiki-plugin-*/client", {cwd: argv.packageDir}, (e, plugins) ->
|
|
249
|
-
plugins.map (plugin) ->
|
|
250
|
-
pluginName = plugin.slice(12, -7)
|
|
251
|
-
pluginPath = '/plugins/' + pluginName
|
|
252
|
-
app.use(pluginPath, cors, express.static(path.join(argv.packageDir, plugin), staticPathOptions))
|
|
253
|
-
|
|
254
|
-
# Add static routes to the security client.
|
|
255
|
-
if argv.security != './security'
|
|
256
|
-
app.use('/security', express.static(path.join(argv.packageDir, argv.security_type, 'client'), staticPathOptions))
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
##### Set up standard environments. #####
|
|
260
|
-
# In dev mode turn on console.log debugging as well as showing the stack on err.
|
|
261
|
-
if 'development' == app.get('env')
|
|
262
|
-
app.use(errorHandler())
|
|
263
|
-
argv.debug = console? and true
|
|
264
|
-
|
|
265
|
-
# Show all of the options a server is using.
|
|
266
|
-
log argv
|
|
267
|
-
|
|
268
|
-
#### Routes ####
|
|
269
|
-
# Routes currently make up the bulk of the Express port of
|
|
270
|
-
# Smallest Federated Wiki. Most routes use literal names,
|
|
271
|
-
# or regexes to match, and then access req.params directly.
|
|
272
|
-
|
|
273
|
-
##### Redirects #####
|
|
274
|
-
# Common redirects that may get used throughout the routes.
|
|
275
|
-
index = argv.home + '.html'
|
|
276
|
-
|
|
277
|
-
oops = '/oops'
|
|
278
|
-
|
|
279
|
-
##### Get routes #####
|
|
280
|
-
# Routes have mostly been kept together by http verb, with the exception
|
|
281
|
-
# of the openID related routes which are at the end together.
|
|
282
|
-
|
|
283
|
-
# Main route for initial contact. Allows us to
|
|
284
|
-
# link into a specific set of pages, local and remote.
|
|
285
|
-
# Can also be handled by the client, but it also sets up
|
|
286
|
-
# the login status, and related footer html, which the client
|
|
287
|
-
# relies on to know if it is logged in or not.
|
|
288
|
-
app.get ///^((/[a-zA-Z0-9:.-]+/[a-z0-9-]+(_rev\d+)?)+)/?$///, cors, (req, res, next) ->
|
|
289
|
-
urlPages = (i for i in req.params[0].split('/') by 2)[1..]
|
|
290
|
-
urlLocs = (j for j in req.params[0].split('/')[1..] by 2)
|
|
291
|
-
if ['plugin', 'auth'].indexOf(urlLocs[0]) > -1
|
|
292
|
-
return next()
|
|
293
|
-
title = urlPages[..].pop().replace(/-+/g,' ')
|
|
294
|
-
user = securityhandler.getUser(req)
|
|
295
|
-
info = {
|
|
296
|
-
title
|
|
297
|
-
pages: []
|
|
298
|
-
authenticated: if user
|
|
299
|
-
true
|
|
300
|
-
else
|
|
301
|
-
false
|
|
302
|
-
user: user
|
|
303
|
-
seedNeighbors: argv.neighbors
|
|
304
|
-
owned: if owner
|
|
305
|
-
true
|
|
306
|
-
else
|
|
307
|
-
false
|
|
308
|
-
isOwner: if securityhandler.isAuthorized(req)
|
|
309
|
-
true
|
|
310
|
-
else
|
|
311
|
-
false
|
|
312
|
-
ownedBy: if owner
|
|
313
|
-
owner
|
|
314
|
-
else
|
|
315
|
-
''
|
|
316
|
-
}
|
|
317
|
-
for page, idx in urlPages
|
|
318
|
-
if urlLocs[idx] is 'view'
|
|
319
|
-
pageDiv = {page}
|
|
320
|
-
else
|
|
321
|
-
pageDiv = {page, origin: """data-site=#{urlLocs[idx]}"""}
|
|
322
|
-
info.pages.push(pageDiv)
|
|
323
|
-
res.render('static.html', info)
|
|
324
|
-
|
|
325
|
-
app.get ///^\/([a-z0-9-]+)\.html$///, cors, (req, res, next) ->
|
|
326
|
-
slug = req.params[0]
|
|
327
|
-
log(slug)
|
|
328
|
-
if slug is 'runtests'
|
|
329
|
-
return next()
|
|
330
|
-
pagehandler.get slug, (e, page, status) ->
|
|
331
|
-
if e then return res.e e
|
|
332
|
-
if status is 404
|
|
333
|
-
return res.status(status).send(page)
|
|
334
|
-
page.title ||= slug.replace(/-+/g,' ')
|
|
335
|
-
page.story ||= []
|
|
336
|
-
user = securityhandler.getUser(req)
|
|
337
|
-
|
|
338
|
-
info = {
|
|
339
|
-
title: page.title
|
|
340
|
-
pages: [
|
|
341
|
-
page: slug
|
|
342
|
-
generated: """data-server-generated=true"""
|
|
343
|
-
story: render(page)
|
|
344
|
-
]
|
|
345
|
-
authenticated: if user
|
|
346
|
-
true
|
|
347
|
-
else
|
|
348
|
-
false
|
|
349
|
-
user: user
|
|
350
|
-
seedNeighbors: argv.neighbors
|
|
351
|
-
owned: if owner
|
|
352
|
-
true
|
|
353
|
-
else
|
|
354
|
-
false
|
|
355
|
-
isOwner: if securityhandler.isAuthorized(req)
|
|
356
|
-
true
|
|
357
|
-
else
|
|
358
|
-
false
|
|
359
|
-
ownedBy: if owner
|
|
360
|
-
owner
|
|
361
|
-
else
|
|
362
|
-
''
|
|
363
|
-
}
|
|
364
|
-
res.render('static.html', info)
|
|
365
|
-
|
|
366
|
-
app.get ///system/factories.json///, (req, res) ->
|
|
367
|
-
res.status(200)
|
|
368
|
-
res.header('Content-Type', 'application/json')
|
|
369
|
-
# Plugins are located in packages in argv.packageDir, with package names of the form wiki-plugin-*
|
|
370
|
-
glob path.join(argv.packageDir, 'wiki-plugin-*', 'factory.json'), (e, files) ->
|
|
371
|
-
if e then return res.e(e)
|
|
372
|
-
|
|
373
|
-
doFactories = (file, cb) ->
|
|
374
|
-
fs.readFile file, (err, data) ->
|
|
375
|
-
return cb() if err
|
|
376
|
-
try
|
|
377
|
-
factory = JSON.parse data
|
|
378
|
-
cb null, factory
|
|
379
|
-
catch err
|
|
380
|
-
return cb()
|
|
381
|
-
|
|
382
|
-
async.map files, doFactories, (e, factories) ->
|
|
383
|
-
res.e(e) if e
|
|
384
|
-
res.end(JSON.stringify factories)
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
###### Json Routes ######
|
|
388
|
-
# Handle fetching local and remote json pages.
|
|
389
|
-
# Local pages are handled by the pagehandler module.
|
|
390
|
-
app.get ///^/([a-z0-9-]+)\.json$///, cors, (req, res) ->
|
|
391
|
-
file = req.params[0]
|
|
392
|
-
pagehandler.get file, (e, page, status) ->
|
|
393
|
-
if e then return res.e e
|
|
394
|
-
res.status(status or 200).send(page)
|
|
395
|
-
|
|
396
|
-
# Remote pages use the http client to retrieve the page
|
|
397
|
-
# and sends it to the client. TODO: consider caching remote pages locally.
|
|
398
|
-
app.get ///^/remote/([a-zA-Z0-9:\.-]+)/([a-z0-9-]+)\.json$///, (req, res) ->
|
|
399
|
-
remoteGet req.params[0], req.params[1], (e, page, status) ->
|
|
400
|
-
if e
|
|
401
|
-
log "remoteGet error:", e
|
|
402
|
-
return res.e e
|
|
403
|
-
res.status(status or 200).send(page)
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
###### Theme Routes ######
|
|
407
|
-
# If themes doesn't exist send 404 and let the client
|
|
408
|
-
# deal with it.
|
|
409
|
-
app.get /^\/theme\/(\w+\.\w+)$/, cors, (req,res) ->
|
|
410
|
-
res.sendFile(path.join(argv.status, 'theme', req.params[0]), (e) ->
|
|
411
|
-
if (e)
|
|
412
|
-
# swallow the error if the theme does not exist...
|
|
413
|
-
if req.path is '/theme/style.css'
|
|
414
|
-
res.set('Content-Type', 'text/css')
|
|
415
|
-
res.send('')
|
|
416
|
-
else
|
|
417
|
-
res.sendStatus(404)
|
|
418
|
-
)
|
|
419
|
-
|
|
420
|
-
###### Favicon Routes ######
|
|
421
|
-
# If favLoc doesn't exist send the default favicon.
|
|
422
|
-
favLoc = path.join(argv.status, 'favicon.png')
|
|
423
|
-
defaultFavLoc = path.join(argv.root, 'default-data', 'status', 'favicon.png')
|
|
424
|
-
app.get '/favicon.png', cors, (req,res) ->
|
|
425
|
-
fs.exists favLoc, (exists) ->
|
|
426
|
-
if exists
|
|
427
|
-
res.sendFile(favLoc)
|
|
428
|
-
else
|
|
429
|
-
res.sendFile(defaultFavLoc)
|
|
430
|
-
|
|
431
|
-
authorized = (req, res, next) ->
|
|
432
|
-
if securityhandler.isAuthorized(req)
|
|
433
|
-
next()
|
|
434
|
-
else
|
|
435
|
-
console.log 'rejecting', req.path
|
|
436
|
-
res.sendStatus(403)
|
|
437
|
-
|
|
438
|
-
# Accept favicon image posted to the server, and if it does not already exist
|
|
439
|
-
# save it.
|
|
440
|
-
app.post '/favicon.png', authorized, (req, res) ->
|
|
441
|
-
favicon = req.body.image.replace(///^data:image/png;base64,///, "")
|
|
442
|
-
buf = new Buffer(favicon, 'base64')
|
|
443
|
-
fs.exists argv.status, (exists) ->
|
|
444
|
-
if exists
|
|
445
|
-
fs.writeFile favLoc, buf, (e) ->
|
|
446
|
-
if e then return res.e e
|
|
447
|
-
res.send('Favicon Saved')
|
|
448
|
-
|
|
449
|
-
else
|
|
450
|
-
fs.mkdir argv.status, { recursive: true }, ->
|
|
451
|
-
fs.writeFile favLoc, buf, (e) ->
|
|
452
|
-
if e then return res.e e
|
|
453
|
-
res.send('Favicon Saved')
|
|
454
|
-
|
|
455
|
-
# Redirect remote favicons to the server they are needed from.
|
|
456
|
-
app.get ///^/remote/([a-zA-Z0-9:\.-]+/favicon.png)$///, (req, res) ->
|
|
457
|
-
remotefav = "http://#{req.params[0]}"
|
|
458
|
-
res.redirect(remotefav)
|
|
459
|
-
|
|
460
|
-
###### Recycler Routes ######
|
|
461
|
-
# These routes are only available to the site's owner
|
|
462
|
-
|
|
463
|
-
# Give the recycler a standard flag - use the Taiwan symbol as the use of
|
|
464
|
-
# negative space outward pointing arrows nicely indicates that items can be removed
|
|
465
|
-
recyclerFavLoc = path.join(argv.root, 'default-data', 'status', 'recycler.png')
|
|
466
|
-
app.get '/recycler/favicon.png', authorized, (req, res) ->
|
|
467
|
-
res.sendFile(recyclerFavLoc)
|
|
468
|
-
|
|
469
|
-
# Send an array of pages currently in the recycler via json
|
|
470
|
-
app.get '/recycler/system/slugs.json', authorized, (req, res) ->
|
|
471
|
-
fs.readdir argv.recycler, (e, files) ->
|
|
472
|
-
|
|
473
|
-
doRecyclermap = (file, cb) ->
|
|
474
|
-
recycleFile = 'recycler/' + file
|
|
475
|
-
pagehandler.get recycleFile, (e, page, status) ->
|
|
476
|
-
if e or status is 404
|
|
477
|
-
console.log 'Problem building recycler map:', file, 'e: ',e
|
|
478
|
-
# this will leave an undefined/empty item in the array, which we will filter out later
|
|
479
|
-
return cb()
|
|
480
|
-
cb null, {
|
|
481
|
-
slug: file
|
|
482
|
-
title: page.title
|
|
483
|
-
}
|
|
484
|
-
|
|
485
|
-
if e then return res.e e
|
|
486
|
-
async.map files, doRecyclermap, (e, recyclermap) ->
|
|
487
|
-
return cb(e) if e
|
|
488
|
-
# remove any empty items
|
|
489
|
-
recyclermap = recyclermap.filter( (el) -> return !!el )
|
|
490
|
-
res.send(recyclermap)
|
|
491
|
-
|
|
492
|
-
# Fetching page from the recycler
|
|
493
|
-
#///^/([a-z0-9-]+)\.json$///
|
|
494
|
-
app.get ///^/recycler/([a-z0-9-]+)\.json$///, authorized, (req, res) ->
|
|
495
|
-
file = 'recycler/' + req.params[0]
|
|
496
|
-
pagehandler.get file, (e, page, status) ->
|
|
497
|
-
if e then return res.e e
|
|
498
|
-
res.status(status or 200).send(page)
|
|
499
|
-
|
|
500
|
-
# Delete page from the recycler
|
|
501
|
-
app.delete ///^/recycler/([a-z0-9-]+)\.json$///, authorized, (req, res) ->
|
|
502
|
-
file = 'recycler/' + req.params[0]
|
|
503
|
-
pagehandler.delete file, (err) ->
|
|
504
|
-
if err then res.status(500).send(err)
|
|
505
|
-
res.status(200).send('')
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
###### Meta Routes ######
|
|
509
|
-
# Send an array of pages in the database via json
|
|
510
|
-
app.get '/system/slugs.json', cors, (req, res) ->
|
|
511
|
-
pagehandler.slugs (err, files) ->
|
|
512
|
-
if err then res.status(500).send(err)
|
|
513
|
-
res.send(files)
|
|
514
|
-
|
|
515
|
-
# Returns a list of installed plugins. (does this get called anymore!)
|
|
516
|
-
app.get '/system/plugins.json', cors, (req, res) ->
|
|
517
|
-
glob "wiki-plugin-*", {cwd: argv.packageDir}, (e, files) ->
|
|
518
|
-
if e then return res.e e
|
|
519
|
-
# extract the plugin name from the name of the directory it's installed in
|
|
520
|
-
files = files.map (file) -> file.slice(12)
|
|
521
|
-
res.send(files)
|
|
522
|
-
|
|
523
|
-
#
|
|
524
|
-
sitemapLoc = path.join(argv.status, 'sitemap.json')
|
|
525
|
-
app.get '/system/sitemap.json', cors, (req, res) ->
|
|
526
|
-
fs.exists sitemapLoc, (exists) ->
|
|
527
|
-
if exists
|
|
528
|
-
res.sendFile(sitemapLoc)
|
|
529
|
-
else
|
|
530
|
-
# only createSitemap if we are not already creating one
|
|
531
|
-
sitemaphandler.createSitemap (pagehandler) if !sitemaphandler.isWorking()
|
|
532
|
-
# wait for the sitemap file to be written, before sending
|
|
533
|
-
sitemaphandler.once 'finished', ->
|
|
534
|
-
res.sendFile(sitemapLoc)
|
|
535
|
-
|
|
536
|
-
xmlSitemapLoc = path.join(argv.status, 'sitemap.xml')
|
|
537
|
-
app.get '/sitemap.xml', cors, (req, res) ->
|
|
538
|
-
fs.exists sitemapLoc, (exists) ->
|
|
539
|
-
if exists
|
|
540
|
-
res.sendFile(xmlSitemapLoc)
|
|
541
|
-
else
|
|
542
|
-
sitemaphandler.createSitemap (pagehandler) if !sitemaphandler.isWorking()
|
|
543
|
-
sitemaphandler.once 'finished', ->
|
|
544
|
-
res.sendFile(xmlSitemapLoc)
|
|
545
|
-
|
|
546
|
-
searchIndexLoc = path.join(argv.status, 'site-index.json')
|
|
547
|
-
app.get '/system/site-index.json', cors, (req, res) ->
|
|
548
|
-
fs.exists searchIndexLoc, (exists) ->
|
|
549
|
-
if exists
|
|
550
|
-
res.sendFile(searchIndexLoc)
|
|
551
|
-
else
|
|
552
|
-
# only create index if we are not already creating one
|
|
553
|
-
searchhandler.createIndex(pagehandler) if !searchhandler.isWorking()
|
|
554
|
-
searchhandler.once 'indexed', ->
|
|
555
|
-
res.sendFile(searchIndexLoc)
|
|
556
|
-
|
|
557
|
-
app.get '/system/export.json', cors, (req, res) ->
|
|
558
|
-
pagehandler.pages (e, sitemap) ->
|
|
559
|
-
return res.e(e) if e
|
|
560
|
-
async.map(
|
|
561
|
-
sitemap,
|
|
562
|
-
(stub, done) ->
|
|
563
|
-
pagehandler.get(stub.slug, (error, page) ->
|
|
564
|
-
return done(e) if e
|
|
565
|
-
done(null, {slug: stub.slug, page})
|
|
566
|
-
)
|
|
567
|
-
,
|
|
568
|
-
(e, pages) ->
|
|
569
|
-
return res.e(e) if e
|
|
570
|
-
res.json(pages.reduce( (dict, combined) ->
|
|
571
|
-
dict[combined.slug] = combined.page
|
|
572
|
-
dict
|
|
573
|
-
, {}))
|
|
574
|
-
)
|
|
575
|
-
|
|
576
|
-
admin = (req, res, next) ->
|
|
577
|
-
if securityhandler.isAdmin(req)
|
|
578
|
-
next()
|
|
579
|
-
else
|
|
580
|
-
console.log 'rejecting', req.path
|
|
581
|
-
res.sendStatus(403)
|
|
582
|
-
|
|
583
|
-
app.get '/system/version.json', admin, (req, res) ->
|
|
584
|
-
versions = {}
|
|
585
|
-
wikiModule = module.parent.parent.parent
|
|
586
|
-
versions[wikiModule.require('./package').name] = wikiModule.require('./package').version
|
|
587
|
-
versions[wikiModule.require('wiki-server/package').name] = wikiModule.require('wiki-server/package').version
|
|
588
|
-
versions[wikiModule.require('wiki-client/package').name] = wikiModule.require('wiki-client/package').version
|
|
589
|
-
versions['security'] = {}
|
|
590
|
-
versions['plugins'] = {}
|
|
591
|
-
|
|
592
|
-
glob '+(wiki-security-*|wiki-plugin-*)', {cwd: argv.packageDir}, (e, plugins) ->
|
|
593
|
-
plugins.map (plugin) ->
|
|
594
|
-
if plugin.includes 'wiki-security'
|
|
595
|
-
versions.security[wikiModule.require(plugin + "/package").name] = wikiModule.require(plugin + "/package").version
|
|
596
|
-
else
|
|
597
|
-
versions.plugins[wikiModule.require(plugin + "/package").name] = wikiModule.require(plugin + "/package").version
|
|
598
|
-
res.json(versions)
|
|
599
|
-
|
|
600
|
-
##### Proxy routes #####
|
|
601
|
-
|
|
602
|
-
app.get '/proxy/*', authorized, (req, res) ->
|
|
603
|
-
pathParts = req.originalUrl.split('/')
|
|
604
|
-
remoteHost = pathParts[2]
|
|
605
|
-
pathParts.splice(0,3)
|
|
606
|
-
remoteResource = pathParts.join('/')
|
|
607
|
-
requestURL = 'http://' + remoteHost + '/' + remoteResource
|
|
608
|
-
console.log("PROXY Request: ", requestURL)
|
|
609
|
-
if requestURL.endsWith('.json') or requestURL.endsWith('.png') or requestURL.endsWith('.jpg') or pathParts[0] is "plugin"
|
|
610
|
-
fetch(requestURL, {timeout: 2000})
|
|
611
|
-
.then (fetchRes) ->
|
|
612
|
-
if fetchRes.ok
|
|
613
|
-
res.set('content-type', fetchRes.headers.get('content-type'))
|
|
614
|
-
res.set('last-modified', fetchRes.headers.get('last-modified'))
|
|
615
|
-
await pipeline(fetchRes.body, res)
|
|
616
|
-
else
|
|
617
|
-
res.status(fetchRes.status).end()
|
|
618
|
-
.catch (err) ->
|
|
619
|
-
console.log("ERROR: Proxy Request ", requestURL, err)
|
|
620
|
-
res.status(500).end()
|
|
621
|
-
else
|
|
622
|
-
res.status(400).end()
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
##### Put routes #####
|
|
626
|
-
|
|
627
|
-
app.put /^\/page\/([a-z0-9-]+)\/action$/i, authorized, (req, res) ->
|
|
628
|
-
action = JSON.parse(req.body.action)
|
|
629
|
-
# Handle all of the possible actions to be taken on a page,
|
|
630
|
-
actionCB = (e, page, status) ->
|
|
631
|
-
#if e then return res.e e
|
|
632
|
-
if status is 404
|
|
633
|
-
# res.status(status).send(page)
|
|
634
|
-
return res.e page,status
|
|
635
|
-
# Using Coffee-Scripts implicit returns we assign page.story to the
|
|
636
|
-
# result of a list comprehension by way of a switch expression.
|
|
637
|
-
try
|
|
638
|
-
page.story = switch action.type
|
|
639
|
-
when 'move'
|
|
640
|
-
action.order.map (id) ->
|
|
641
|
-
page.story.filter((para) ->
|
|
642
|
-
id == para.id
|
|
643
|
-
)[0] or throw('Ignoring move. Try reload.')
|
|
644
|
-
|
|
645
|
-
when 'add'
|
|
646
|
-
idx = page.story.map((para) -> para.id).indexOf(action.after) + 1
|
|
647
|
-
page.story.splice(idx, 0, action.item)
|
|
648
|
-
page.story
|
|
649
|
-
|
|
650
|
-
when 'remove'
|
|
651
|
-
page.story.filter (para) ->
|
|
652
|
-
para?.id != action.id
|
|
653
|
-
|
|
654
|
-
when 'edit'
|
|
655
|
-
page.story.map (para) ->
|
|
656
|
-
if para.id is action.id
|
|
657
|
-
action.item
|
|
658
|
-
else
|
|
659
|
-
para
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
when 'create', 'fork'
|
|
663
|
-
page.story or []
|
|
664
|
-
|
|
665
|
-
else
|
|
666
|
-
log "Unfamiliar action:", action
|
|
667
|
-
#page.story
|
|
668
|
-
throw('Unfamiliar action ignored')
|
|
669
|
-
catch e
|
|
670
|
-
return res.e e
|
|
671
|
-
|
|
672
|
-
# Add a blank journal if it does not exist.
|
|
673
|
-
# And add what happened to the journal.
|
|
674
|
-
if not page.journal
|
|
675
|
-
page.journal = []
|
|
676
|
-
if action.fork
|
|
677
|
-
page.journal.push({type: "fork", site: action.fork, date: action.date - 1})
|
|
678
|
-
delete action.fork
|
|
679
|
-
page.journal.push(action)
|
|
680
|
-
pagehandler.put req.params[0], page, (e) ->
|
|
681
|
-
if e then return res.e e
|
|
682
|
-
res.send('ok')
|
|
683
|
-
# log 'saved'
|
|
684
|
-
|
|
685
|
-
# update sitemap
|
|
686
|
-
sitemaphandler.update(req.params[0], page)
|
|
687
|
-
|
|
688
|
-
# update site index
|
|
689
|
-
searchhandler.update(req.params[0], page)
|
|
690
|
-
|
|
691
|
-
# log action
|
|
692
|
-
|
|
693
|
-
# If the action is a fork, get the page from the remote server,
|
|
694
|
-
# otherwise ask pagehandler for it.
|
|
695
|
-
if action.fork
|
|
696
|
-
pagehandler.saveToRecycler req.params[0], (err) ->
|
|
697
|
-
if err and err isnt 'page does not exist'
|
|
698
|
-
console.log "Error saving #{req.params[0]} before fork: #{err}"
|
|
699
|
-
if action.forkPage
|
|
700
|
-
forkPageCopy = JSON.parse(JSON.stringify(action.forkPage))
|
|
701
|
-
delete action.forkPage
|
|
702
|
-
actionCB(null, forkPageCopy)
|
|
703
|
-
else
|
|
704
|
-
# Legacy path, new clients will provide forkPage on implicit forks.
|
|
705
|
-
remoteGet(action.fork, req.params[0], actionCB)
|
|
706
|
-
else if action.type is 'create'
|
|
707
|
-
# Prevent attempt to write circular structure
|
|
708
|
-
itemCopy = JSON.parse(JSON.stringify(action.item))
|
|
709
|
-
pagehandler.get req.params[0], (e, page, status) ->
|
|
710
|
-
if e then return actionCB(e)
|
|
711
|
-
unless status is 404
|
|
712
|
-
res.status(409).send('Page already exists.')
|
|
713
|
-
else
|
|
714
|
-
actionCB(null, itemCopy)
|
|
715
|
-
|
|
716
|
-
else if action.type == 'fork'
|
|
717
|
-
pagehandler.saveToRecycler req.params[0], (err) ->
|
|
718
|
-
if err then console.log "Error saving #{req.params[0]} before fork: #{err}"
|
|
719
|
-
if action.forkPage # push
|
|
720
|
-
forkPageCopy = JSON.parse(JSON.stringify(action.forkPage))
|
|
721
|
-
delete action.forkPage
|
|
722
|
-
actionCB(null, forkPageCopy)
|
|
723
|
-
else # pull
|
|
724
|
-
remoteGet(action.site, req.params[0], actionCB)
|
|
725
|
-
else
|
|
726
|
-
pagehandler.get(req.params[0], actionCB)
|
|
727
|
-
|
|
728
|
-
# Return the oops page when login fails.
|
|
729
|
-
app.get '/oops', (req, res) ->
|
|
730
|
-
res.statusCode = 403
|
|
731
|
-
res.render('oops.html', {msg:'This is not your wiki!'})
|
|
732
|
-
|
|
733
|
-
# Traditional request to / redirects to index :)
|
|
734
|
-
app.get '/', cors, (req, res) ->
|
|
735
|
-
home = path.join argv.assets, 'home', 'index.html'
|
|
736
|
-
fs.stat home, (err, stats) ->
|
|
737
|
-
if err || !stats.isFile()
|
|
738
|
-
res.redirect(index)
|
|
739
|
-
else
|
|
740
|
-
res.redirect("/assets/home/index.html")
|
|
741
|
-
|
|
742
|
-
##### Delete Routes #####
|
|
743
|
-
|
|
744
|
-
app.delete ///^/([a-z0-9-]+)\.json$///, authorized, (req, res) ->
|
|
745
|
-
pageFile = req.params[0]
|
|
746
|
-
# we need the original page text to remove it from the index, so get the original text before deleting it
|
|
747
|
-
pagehandler.get pageFile, (e, page, status) ->
|
|
748
|
-
title = page.title
|
|
749
|
-
pagehandler.delete pageFile, (err) ->
|
|
750
|
-
if err
|
|
751
|
-
res.status(500).send(err)
|
|
752
|
-
else
|
|
753
|
-
sitemaphandler.removePage pageFile
|
|
754
|
-
res.status(200).send('')
|
|
755
|
-
# update site index
|
|
756
|
-
searchhandler.removePage(req.params[0])
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
#### Start the server ####
|
|
761
|
-
# Wait to make sure owner is known before listening.
|
|
762
|
-
securityhandler.retrieveOwner (e) ->
|
|
763
|
-
# Throw if you can't find the initial owner
|
|
764
|
-
if e then throw e
|
|
765
|
-
owner = securityhandler.getOwner()
|
|
766
|
-
console.log "owner: " + owner
|
|
767
|
-
app.emit 'owner-set'
|
|
768
|
-
|
|
769
|
-
app.on 'running-serv', (server) ->
|
|
770
|
-
### Plugins ###
|
|
771
|
-
# Should replace most WebSocketServers below.
|
|
772
|
-
plugins = pluginsFactory(argv)
|
|
773
|
-
plugins.startServers({argv, app})
|
|
774
|
-
### Sitemap ###
|
|
775
|
-
# create sitemap at start-up
|
|
776
|
-
sitemaphandler.createSitemap(pagehandler)
|
|
777
|
-
# create site index at start-up
|
|
778
|
-
searchhandler.startUp(pagehandler)
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
# Return app when called, so that it can be watched for events and shutdown with .close() externally.
|
|
782
|
-
app
|