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/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