diff --git a/core/modules/server/server.js b/core/modules/server/server.js index 1243190f4..258ddfa31 100644 --- a/core/modules/server/server.js +++ b/core/modules/server/server.js @@ -74,6 +74,7 @@ function Server(options) { }); // Load route handlers $tw.modules.forEachModuleOfType("route", function(title,routeDefinition) { + // console.log("Loading server route " + title); self.addRoute(routeDefinition); }); // Initialise the http vs https @@ -162,108 +163,6 @@ function sendResponse(request,response,statusCode,headers,data,encoding) { response.end(data,encoding); } -function redirect(request,response,statusCode,location) { - response.setHeader("Location",location); - response.statusCode = statusCode; - response.end() -} - -/* -Options include: -cbPartStart(headers,name,filename) - invoked when a file starts being received -cbPartChunk(chunk) - invoked when a chunk of a file is received -cbPartEnd() - invoked when a file finishes being received -cbFinished(err) - invoked when the all the form data has been processed -*/ -function streamMultipartData(request,options) { - // Check that the Content-Type is multipart/form-data - const contentType = request.headers['content-type']; - if(!contentType.startsWith("multipart/form-data")) { - return options.cbFinished("Expected multipart/form-data content type"); - } - // Extract the boundary string from the Content-Type header - const boundaryMatch = contentType.match(/boundary=(.+)$/); - if(!boundaryMatch) { - return options.cbFinished("Missing boundary in multipart/form-data"); - } - const boundary = boundaryMatch[1]; - const boundaryBuffer = Buffer.from("--" + boundary); - // Initialise - let buffer = Buffer.alloc(0); - let processingPart = false; - // Process incoming chunks - request.on("data", (chunk) => { - // Accumulate the incoming data - buffer = Buffer.concat([buffer, chunk]); - // Loop through any parts within the current buffer - while (true) { - if(!processingPart) { - // If we're not processing a part then we try to find a boundary marker - const boundaryIndex = buffer.indexOf(boundaryBuffer); - if(boundaryIndex === -1) { - // Haven't reached the boundary marker yet, so we should wait for more data - break; - } - // Look for the end of the headers - const endOfHeaders = buffer.indexOf("\r\n\r\n",boundaryIndex + boundaryBuffer.length); - if(endOfHeaders === -1) { - // Haven't reached the end of the headers, so we should wait for more data - break; - } - // Extract and parse headers - const headersPart = Uint8Array.prototype.slice.call(buffer,boundaryIndex + boundaryBuffer.length,endOfHeaders).toString(); - const currentHeaders = {}; - headersPart.split("\r\n").forEach(headerLine => { - const [key, value] = headerLine.split(": "); - currentHeaders[key.toLowerCase()] = value; - }); - // Parse the content disposition header - const contentDisposition = { - name: null, - filename: null - }; - if(currentHeaders["content-disposition"]) { - // Split the content-disposition header into semicolon-delimited parts - const parts = currentHeaders["content-disposition"].split(";").map(part => part.trim()); - // Iterate over each part to extract name and filename if they exist - parts.forEach(part => { - if(part.startsWith("name=")) { - // Remove "name=" and trim quotes - contentDisposition.name = part.substring(6,part.length - 1); - } else if(part.startsWith("filename=")) { - // Remove "filename=" and trim quotes - contentDisposition.filename = part.substring(10,part.length - 1); - } - }); - } - processingPart = true; - options.cbPartStart(currentHeaders,contentDisposition.name,contentDisposition.filename); - // Slice the buffer to the next part - buffer = Uint8Array.prototype.slice.call(buffer,endOfHeaders + 4); - } else { - const boundaryIndex = buffer.indexOf(boundaryBuffer); - if(boundaryIndex >= 0) { - // Return the part up to the boundary minus the terminating LF CR - options.cbPartChunk(Uint8Array.prototype.slice.call(buffer,0,boundaryIndex - 2)); - options.cbPartEnd(); - processingPart = false; - buffer = Uint8Array.prototype.slice.call(buffer,boundaryIndex); - } else { - // Return the rest of the buffer - options.cbPartChunk(buffer); - // Reset the buffer and wait for more data - buffer = Buffer.alloc(0); - break; - } - } - } - }); - // All done - request.on("end", () => { - options.cbFinished(null); - }); -} - Server.prototype.defaultVariables = { port: "8080", host: "127.0.0.1", @@ -356,10 +255,14 @@ Server.prototype.requestHandler = function(request,response,options) { state.queryParameters = querystring.parse(state.urlInfo.query); state.pathPrefix = options.pathPrefix || this.get("path-prefix") || ""; state.sendResponse = sendResponse.bind(self,request,response); - state.redirect = redirect.bind(self,request,response); - state.streamMultipartData = streamMultipartData.bind(self,request); // Get the principals authorized to access this resource state.authorizationType = options.authorizationType || this.methodMappings[request.method] || "readers"; + // Check for the CSRF header if this is a write + if(!this.csrfDisable && state.authorizationType === "writers" && request.headers["x-requested-with"] !== "TiddlyWiki") { + response.writeHead(403,"'X-Requested-With' header required to login to '" + this.servername + "'"); + response.end(); + return; + } // Check whether anonymous access is granted state.allowAnon = this.isAuthorized(state.authorizationType,null); // Authenticate with the first active authenticator @@ -389,12 +292,6 @@ Server.prototype.requestHandler = function(request,response,options) { response.end(); return; } - // If this is a write, check for the CSRF header unless globally disabled, or disabled for this route - if(!this.csrfDisable && !route.csrfDisable && state.authorizationType === "writers" && request.headers["x-requested-with"] !== "TiddlyWiki") { - response.writeHead(403,"'X-Requested-With' header required to login to '" + this.servername + "'"); - response.end(); - return; - } // Receive the request body if necessary and hand off to the route handler if(route.bodyFormat === "stream" || request.method === "GET" || request.method === "HEAD") { // Let the route handle the request stream itself @@ -430,9 +327,8 @@ Listen for requests port: optional port number (falls back to value of "port" variable) host: optional host address (falls back to value of "host" variable) prefix: optional prefix (falls back to value of "path-prefix" variable) -callback: optional callback(err) to be invoked when the listener is up and running */ -Server.prototype.listen = function(port,host,prefix,options) { +Server.prototype.listen = function(port,host,prefix) { var self = this; // Handle defaults for port and host port = port || this.get("port"); @@ -445,7 +341,7 @@ Server.prototype.listen = function(port,host,prefix,options) { // Warn if required plugins are missing var missing = []; for (var index=0; index 2048)) { + var acceptEncoding = request.headers["accept-encoding"] || ""; + if(/\bdeflate\b/.test(acceptEncoding)) { + headers["Content-Encoding"] = "deflate"; + data = zlib.deflateSync(data); + } else if(/\bgzip\b/.test(acceptEncoding)) { + headers["Content-Encoding"] = "gzip"; + data = zlib.gzipSync(data); + } + } + + response.writeHead(statusCode,headers); + response.end(data,encoding); +} + +function redirect(request,response,statusCode,location) { + response.setHeader("Location",location); + response.statusCode = statusCode; + response.end() +} + +/* +Options include: +cbPartStart(headers,name,filename) - invoked when a file starts being received +cbPartChunk(chunk) - invoked when a chunk of a file is received +cbPartEnd() - invoked when a file finishes being received +cbFinished(err) - invoked when the all the form data has been processed +*/ +function streamMultipartData(request,options) { + // Check that the Content-Type is multipart/form-data + const contentType = request.headers['content-type']; + if(!contentType.startsWith("multipart/form-data")) { + return options.cbFinished("Expected multipart/form-data content type"); + } + // Extract the boundary string from the Content-Type header + const boundaryMatch = contentType.match(/boundary=(.+)$/); + if(!boundaryMatch) { + return options.cbFinished("Missing boundary in multipart/form-data"); + } + const boundary = boundaryMatch[1]; + const boundaryBuffer = Buffer.from("--" + boundary); + // Initialise + let buffer = Buffer.alloc(0); + let processingPart = false; + // Process incoming chunks + request.on("data", (chunk) => { + // Accumulate the incoming data + buffer = Buffer.concat([buffer, chunk]); + // Loop through any parts within the current buffer + while (true) { + if(!processingPart) { + // If we're not processing a part then we try to find a boundary marker + const boundaryIndex = buffer.indexOf(boundaryBuffer); + if(boundaryIndex === -1) { + // Haven't reached the boundary marker yet, so we should wait for more data + break; + } + // Look for the end of the headers + const endOfHeaders = buffer.indexOf("\r\n\r\n",boundaryIndex + boundaryBuffer.length); + if(endOfHeaders === -1) { + // Haven't reached the end of the headers, so we should wait for more data + break; + } + // Extract and parse headers + const headersPart = Uint8Array.prototype.slice.call(buffer,boundaryIndex + boundaryBuffer.length,endOfHeaders).toString(); + const currentHeaders = {}; + headersPart.split("\r\n").forEach(headerLine => { + const [key, value] = headerLine.split(": "); + currentHeaders[key.toLowerCase()] = value; + }); + // Parse the content disposition header + const contentDisposition = { + name: null, + filename: null + }; + if(currentHeaders["content-disposition"]) { + // Split the content-disposition header into semicolon-delimited parts + const parts = currentHeaders["content-disposition"].split(";").map(part => part.trim()); + // Iterate over each part to extract name and filename if they exist + parts.forEach(part => { + if(part.startsWith("name=")) { + // Remove "name=" and trim quotes + contentDisposition.name = part.substring(6,part.length - 1); + } else if(part.startsWith("filename=")) { + // Remove "filename=" and trim quotes + contentDisposition.filename = part.substring(10,part.length - 1); + } + }); + } + processingPart = true; + options.cbPartStart(currentHeaders,contentDisposition.name,contentDisposition.filename); + // Slice the buffer to the next part + buffer = Uint8Array.prototype.slice.call(buffer,endOfHeaders + 4); + } else { + const boundaryIndex = buffer.indexOf(boundaryBuffer); + if(boundaryIndex >= 0) { + // Return the part up to the boundary minus the terminating LF CR + options.cbPartChunk(Uint8Array.prototype.slice.call(buffer,0,boundaryIndex - 2)); + options.cbPartEnd(); + processingPart = false; + buffer = Uint8Array.prototype.slice.call(buffer,boundaryIndex); + } else { + // Return the rest of the buffer + options.cbPartChunk(buffer); + // Reset the buffer and wait for more data + buffer = Buffer.alloc(0); + break; + } + } + } + }); + // All done + request.on("end", () => { + options.cbFinished(null); + }); +} + +Server.prototype.defaultVariables = { + port: "8080", + host: "127.0.0.1", + "required-plugins": "$:/plugins/tiddlywiki/filesystem,$:/plugins/tiddlywiki/tiddlyweb", + "root-tiddler": "$:/core/save/all", + "root-render-type": "text/plain", + "root-serve-type": "text/html", + "tiddler-render-type": "text/html", + "tiddler-render-template": "$:/core/templates/server/static.tiddler.html", + "system-tiddler-render-type": "text/plain", + "system-tiddler-render-template": "$:/core/templates/wikified-tiddler", + "debug-level": "none", + "gzip": "no", + "use-browser-cache": "no" +}; + +Server.prototype.get = function(name) { + return this.variables[name]; +}; + +Server.prototype.addRoute = function(route) { + this.routes.push(route); +}; + +Server.prototype.addAuthenticator = function(AuthenticatorClass) { + // Instantiate and initialise the authenticator + var authenticator = new AuthenticatorClass(this), + result = authenticator.init(); + if(typeof result === "string") { + $tw.utils.error("Error: " + result); + } else if(result) { + // Only use the authenticator if it initialised successfully + this.authenticators.push(authenticator); + } +}; + +Server.prototype.findMatchingRoute = function(request,state) { + for(var t=0; t 0) { + if(!this.authenticators[0].authenticateRequest(request,response,state)) { + // Bail if we failed (the authenticator will have sent the response) + return; + } + } + // Authorize with the authenticated username + if(!this.isAuthorized(state.authorizationType,state.authenticatedUsername)) { + response.writeHead(401,"'" + state.authenticatedUsername + "' is not authorized to access '" + this.servername + "'"); + response.end(); + return; + } + // Find the route that matches this path + var route = self.findMatchingRoute(request,state); + // Optionally output debug info + if(self.get("debug-level") !== "none") { + console.log("Request path:",JSON.stringify(state.urlInfo)); + console.log("Request headers:",JSON.stringify(request.headers)); + console.log("authenticatedUsername:",state.authenticatedUsername); + } + // Return a 404 if we didn't find a route + if(!route) { + response.writeHead(404); + response.end(); + return; + } + // If this is a write, check for the CSRF header unless globally disabled, or disabled for this route + if(!this.csrfDisable && !route.csrfDisable && state.authorizationType === "writers" && request.headers["x-requested-with"] !== "TiddlyWiki") { + response.writeHead(403,"'X-Requested-With' header required to login to '" + this.servername + "'"); + response.end(); + return; + } + // Receive the request body if necessary and hand off to the route handler + if(route.bodyFormat === "stream" || request.method === "GET" || request.method === "HEAD") { + // Let the route handle the request stream itself + route.handler(request,response,state); + } else if(route.bodyFormat === "string" || !route.bodyFormat) { + // Set the encoding for the incoming request + request.setEncoding("utf8"); + var data = ""; + request.on("data",function(chunk) { + data += chunk.toString(); + }); + request.on("end",function() { + state.data = data; + route.handler(request,response,state); + }); + } else if(route.bodyFormat === "buffer") { + var data = []; + request.on("data",function(chunk) { + data.push(chunk); + }); + request.on("end",function() { + state.data = Buffer.concat(data); + route.handler(request,response,state); + }) + } else { + response.writeHead(400,"Invalid bodyFormat " + route.bodyFormat + " in route " + route.method + " " + route.path.source); + response.end(); + } +}; + +/* +Listen for requests +port: optional port number (falls back to value of "port" variable) +host: optional host address (falls back to value of "host" variable) +prefix: optional prefix (falls back to value of "path-prefix" variable) +callback: optional callback(err) to be invoked when the listener is up and running +*/ +Server.prototype.listen = function(port,host,prefix,options) { + var self = this; + // Handle defaults for port and host + port = port || this.get("port"); + host = host || this.get("host"); + prefix = prefix || this.get("path-prefix") || ""; + // Check for the port being a string and look it up as an environment variable + if(parseInt(port,10).toString() !== port) { + port = process.env[port] || 8080; + } + // Warn if required plugins are missing + var missing = []; + for (var index=0; index 0) { + var error = "Warning: Plugin(s) required for client-server operation are missing.\n"+ + "\""+ missing.join("\", \"")+"\""; + $tw.utils.warning(error); + } + // Create the server + var server = this.transport.createServer(this.listenOptions || {},function(request,response,options) { + if(self.get("debug-level") !== "none") { + var start = $tw.utils.timer(); + response.on("finish",function() { + console.log("Response time:",request.method,request.url,$tw.utils.timer() - start); + }); + } + self.requestHandler(request,response,options); + }); + // Display the port number after we've started listening (the port number might have been specified as zero, in which case we will get an assigned port) + server.on("listening",function() { + var address = server.address(), + url = self.protocol + "://" + (address.family === "IPv6" ? "[" + address.address + "]" : address.address) + ":" + address.port + prefix; + $tw.utils.log("Serving on " + url,"brown/orange"); + $tw.utils.log("(press ctrl-C to exit)","red"); + if(options.callback) { + options.callback(null); + } + }); + // Listen + return server.listen(port,host); +}; + +exports.Server = Server; + +})(); diff --git a/plugins/tiddlywiki/multiwikiserver/modules/routes/handlers/delete-recipe-tiddler.js b/plugins/tiddlywiki/multiwikiserver/modules/routes/handlers/delete-recipe-tiddler.js index e2d786350..06279ce13 100644 --- a/plugins/tiddlywiki/multiwikiserver/modules/routes/handlers/delete-recipe-tiddler.js +++ b/plugins/tiddlywiki/multiwikiserver/modules/routes/handlers/delete-recipe-tiddler.js @@ -1,7 +1,7 @@ /*\ title: $:/plugins/tiddlywiki/multiwikiserver/routes/handlers/delete-recipe-tiddler.js type: application/javascript -module-type: route +module-type: mws-route DELETE /wiki/:recipe_name/recipes/:bag_name/tiddler/:title diff --git a/plugins/tiddlywiki/multiwikiserver/modules/routes/handlers/get-bag-tiddler-blob.js b/plugins/tiddlywiki/multiwikiserver/modules/routes/handlers/get-bag-tiddler-blob.js index b2e5c64b8..90dbe196b 100644 --- a/plugins/tiddlywiki/multiwikiserver/modules/routes/handlers/get-bag-tiddler-blob.js +++ b/plugins/tiddlywiki/multiwikiserver/modules/routes/handlers/get-bag-tiddler-blob.js @@ -1,7 +1,7 @@ /*\ title: $:/plugins/tiddlywiki/multiwikiserver/routes/handlers/get-bag-tiddler-blob.js type: application/javascript -module-type: route +module-type: mws-route GET /wiki/:bag_name/bags/:bag_name/tiddler/:title/blob diff --git a/plugins/tiddlywiki/multiwikiserver/modules/routes/handlers/get-bag-tiddler.js b/plugins/tiddlywiki/multiwikiserver/modules/routes/handlers/get-bag-tiddler.js index 9c3906843..9db23c3ed 100644 --- a/plugins/tiddlywiki/multiwikiserver/modules/routes/handlers/get-bag-tiddler.js +++ b/plugins/tiddlywiki/multiwikiserver/modules/routes/handlers/get-bag-tiddler.js @@ -1,7 +1,7 @@ /*\ title: $:/plugins/tiddlywiki/multiwikiserver/routes/handlers/get-bag-tiddler.js type: application/javascript -module-type: route +module-type: mws-route GET /wiki/:bag_name/bags/:bag_name/tiddler/:title diff --git a/plugins/tiddlywiki/multiwikiserver/modules/routes/handlers/get-bag.js b/plugins/tiddlywiki/multiwikiserver/modules/routes/handlers/get-bag.js index afccbad9f..cc74f4341 100644 --- a/plugins/tiddlywiki/multiwikiserver/modules/routes/handlers/get-bag.js +++ b/plugins/tiddlywiki/multiwikiserver/modules/routes/handlers/get-bag.js @@ -1,7 +1,7 @@ /*\ title: $:/plugins/tiddlywiki/multiwikiserver/routes/handlers/get-bag.js type: application/javascript -module-type: route +module-type: mws-route GET /wiki/:bag_name/bags/:bag_name/ GET /wiki/:bag_name/bags/:bag_name diff --git a/plugins/tiddlywiki/multiwikiserver/modules/routes/handlers/get-recipe-tiddler.js b/plugins/tiddlywiki/multiwikiserver/modules/routes/handlers/get-recipe-tiddler.js index 79c350fd7..4a1eee781 100644 --- a/plugins/tiddlywiki/multiwikiserver/modules/routes/handlers/get-recipe-tiddler.js +++ b/plugins/tiddlywiki/multiwikiserver/modules/routes/handlers/get-recipe-tiddler.js @@ -1,7 +1,7 @@ /*\ title: $:/plugins/tiddlywiki/multiwikiserver/routes/handlers/get-recipe-tiddler.js type: application/javascript -module-type: route +module-type: mws-route GET /wiki/:recipe_name/recipes/:recipe_name/tiddler/:title diff --git a/plugins/tiddlywiki/multiwikiserver/modules/routes/handlers/get-recipe-tiddlers-json.js b/plugins/tiddlywiki/multiwikiserver/modules/routes/handlers/get-recipe-tiddlers-json.js index 69b41aa68..ebab92699 100644 --- a/plugins/tiddlywiki/multiwikiserver/modules/routes/handlers/get-recipe-tiddlers-json.js +++ b/plugins/tiddlywiki/multiwikiserver/modules/routes/handlers/get-recipe-tiddlers-json.js @@ -1,7 +1,7 @@ /*\ title: $:/plugins/tiddlywiki/multiwikiserver/routes/handlers/get-recipe-tiddlers-json.js type: application/javascript -module-type: route +module-type: mws-route GET /wiki/:recipe_name/recipes/:recipe_name/tiddlers.json?filter=:filter diff --git a/plugins/tiddlywiki/multiwikiserver/modules/routes/handlers/get-recipe.js b/plugins/tiddlywiki/multiwikiserver/modules/routes/handlers/get-recipe.js index 05370b832..1ad3ddfe6 100644 --- a/plugins/tiddlywiki/multiwikiserver/modules/routes/handlers/get-recipe.js +++ b/plugins/tiddlywiki/multiwikiserver/modules/routes/handlers/get-recipe.js @@ -1,7 +1,7 @@ /*\ title: $:/plugins/tiddlywiki/multiwikiserver/routes/handlers/get-recipe.js type: application/javascript -module-type: route +module-type: mws-route GET /wiki/:recipe_name diff --git a/plugins/tiddlywiki/multiwikiserver/modules/routes/handlers/get-status.js b/plugins/tiddlywiki/multiwikiserver/modules/routes/handlers/get-status.js index 021bea7b6..1ec927f73 100644 --- a/plugins/tiddlywiki/multiwikiserver/modules/routes/handlers/get-status.js +++ b/plugins/tiddlywiki/multiwikiserver/modules/routes/handlers/get-status.js @@ -1,7 +1,7 @@ /*\ title: $:/plugins/tiddlywiki/multiwikiserver/routes/handlers/get-status.js type: application/javascript -module-type: route +module-type: mws-route GET /wiki/:recipe_name/status diff --git a/plugins/tiddlywiki/multiwikiserver/modules/routes/handlers/post-bag-tiddlers.js b/plugins/tiddlywiki/multiwikiserver/modules/routes/handlers/post-bag-tiddlers.js index 972e0741a..a4a95183e 100644 --- a/plugins/tiddlywiki/multiwikiserver/modules/routes/handlers/post-bag-tiddlers.js +++ b/plugins/tiddlywiki/multiwikiserver/modules/routes/handlers/post-bag-tiddlers.js @@ -1,7 +1,7 @@ /*\ title: $:/plugins/tiddlywiki/multiwikiserver/routes/handlers/post-bag-tiddlers.js type: application/javascript -module-type: route +module-type: mws-route POST /wiki/:bag_name/bags/:bag_name/tiddlers/ diff --git a/plugins/tiddlywiki/multiwikiserver/modules/routes/handlers/put-bag.js b/plugins/tiddlywiki/multiwikiserver/modules/routes/handlers/put-bag.js index 9dd13f0f7..22ffcaa7c 100644 --- a/plugins/tiddlywiki/multiwikiserver/modules/routes/handlers/put-bag.js +++ b/plugins/tiddlywiki/multiwikiserver/modules/routes/handlers/put-bag.js @@ -1,7 +1,7 @@ /*\ title: $:/plugins/tiddlywiki/multiwikiserver/routes/handlers/put-bag.js type: application/javascript -module-type: route +module-type: mws-route PUT /wiki/:bag_name/bags/:bag_name diff --git a/plugins/tiddlywiki/multiwikiserver/modules/routes/handlers/put-recipe-tiddler.js b/plugins/tiddlywiki/multiwikiserver/modules/routes/handlers/put-recipe-tiddler.js index 3a1fab92a..69dbb685d 100644 --- a/plugins/tiddlywiki/multiwikiserver/modules/routes/handlers/put-recipe-tiddler.js +++ b/plugins/tiddlywiki/multiwikiserver/modules/routes/handlers/put-recipe-tiddler.js @@ -1,7 +1,7 @@ /*\ title: $:/plugins/tiddlywiki/multiwikiserver/routes/handlers/put-recipe-tiddler.js type: application/javascript -module-type: route +module-type: mws-route PUT /wiki/:recipe_name/recipes/:recipe_name/tiddlers/:title diff --git a/plugins/tiddlywiki/multiwikiserver/modules/routes/handlers/put-recipe.js b/plugins/tiddlywiki/multiwikiserver/modules/routes/handlers/put-recipe.js index 1b2c0d175..bff94699a 100644 --- a/plugins/tiddlywiki/multiwikiserver/modules/routes/handlers/put-recipe.js +++ b/plugins/tiddlywiki/multiwikiserver/modules/routes/handlers/put-recipe.js @@ -1,7 +1,7 @@ /*\ title: $:/plugins/tiddlywiki/multiwikiserver/routes/handlers/put-recipe.js type: application/javascript -module-type: route +module-type: mws-route PUT /wiki/:recipe_name/recipes/:recipe_name diff --git a/plugins/tiddlywiki/multiwikiserver/modules/startup.js b/plugins/tiddlywiki/multiwikiserver/modules/startup.js new file mode 100644 index 000000000..8a9c97921 --- /dev/null +++ b/plugins/tiddlywiki/multiwikiserver/modules/startup.js @@ -0,0 +1,100 @@ +/*\ +title: $:/plugins/tiddlywiki/multiwikiserver/startup.js +type: application/javascript +module-type: startup + +Multi wiki server initialisation + +\*/ +(function(){ + +/*jslint node: true, browser: true */ +/*global $tw: false */ +"use strict"; + +// Export name and synchronous status +exports.name = "multiwikiserver"; +exports.platforms = ["node"]; +exports.before = ["story"]; +exports.synchronous = true; + +exports.startup = function() { + const store = setupStore(); + loadStore(store); + $tw.mws = { + store: store, + serverManager: new ServerManager({ + store: store + }) + }; +} + +function setupStore() { + const path = require("path"); + // Create and initialise the attachment store and the tiddler store + const AttachmentStore = require("$:/plugins/tiddlywiki/multiwikiserver/store/attachments.js").AttachmentStore, + attachmentStore = new AttachmentStore({ + storePath: path.resolve($tw.boot.wikiPath,"store/") + }), + SqlTiddlerStore = require("$:/plugins/tiddlywiki/multiwikiserver/store/sql-tiddler-store.js").SqlTiddlerStore, + store = new SqlTiddlerStore({ + databasePath: path.resolve($tw.boot.wikiPath,"store/database.sqlite"), + engine: $tw.wiki.getTiddlerText("$:/config/MultiWikiServer/Engine","better"), // better || wasm + attachmentStore: attachmentStore + }); + return store; +} + +function loadStore(store) { + const path = require("path"); + // Performance timing + console.time("mws-initial-load"); + // Copy TiddlyWiki core editions + function copyEdition(options) { + console.log(`Copying edition ${options.tiddlersPath}`); + store.createBag(options.bagName,options.bagDescription); + store.createRecipe(options.recipeName,[options.bagName],options.recipeDescription); + store.saveTiddlersFromPath(path.resolve($tw.boot.corePath,$tw.config.editionsPath,options.tiddlersPath),options.bagName); + } + copyEdition({ + bagName: "docs", + bagDescription: "TiddlyWiki Documentation from https://tiddlywiki.com", + recipeName: "docs", + recipeDescription: "TiddlyWiki Documentation from https://tiddlywiki.com", + tiddlersPath: "tw5.com/tiddlers" + }); + copyEdition({ + bagName: "dev-docs", + bagDescription: "TiddlyWiki Developer Documentation from https://tiddlywiki.com/dev", + recipeName: "dev-docs", + recipeDescription: "TiddlyWiki Developer Documentation from https://tiddlywiki.com/dev", + tiddlersPath: "dev/tiddlers" + }); + // Create bags and recipes + store.createBag("bag-alpha","A test bag"); + store.createBag("bag-beta","Another test bag"); + store.createBag("bag-gamma","A further test bag"); + store.createRecipe("recipe-rho",["bag-alpha","bag-beta"],"First wiki"); + store.createRecipe("recipe-sigma",["bag-alpha","bag-gamma"],"Second Wiki"); + store.createRecipe("recipe-tau",["bag-alpha"],"Third Wiki"); + store.createRecipe("recipe-upsilon",["bag-alpha","bag-gamma","bag-beta"],"Fourth Wiki"); + // Save tiddlers + store.saveBagTiddler({title: "$:/SiteTitle",text: "Bag Alpha"},"bag-alpha"); + store.saveBagTiddler({title: "😀😃😄😁😆🥹😅😂",text: "Bag Alpha"},"bag-alpha"); + store.saveBagTiddler({title: "$:/SiteTitle",text: "Bag Beta"},"bag-beta"); + store.saveBagTiddler({title: "$:/SiteTitle",text: "Bag Gamma"},"bag-gamma"); + console.timeEnd("mws-initial-load"); +} + +function ServerManager(store) { + this.servers = []; +} + +ServerManager.prototype.createServer = function(options) { + const MWSServer = require("$:/plugins/tiddlywiki/multiwikiserver/mws-server.js").Server, + server = new MWSServer(options); + this.servers.push(server); + return server; +} + +})();