contrib/nitiwiki: complete the nitcorn server to modify a nitiwiki
authorAlexis Laferrière <alexis.laf@xymus.net>
Fri, 4 Dec 2015 17:45:31 +0000 (12:45 -0500)
committerAlexis Laferrière <alexis.laf@xymus.net>
Sun, 6 Dec 2015 19:32:29 +0000 (14:32 -0500)
Signed-off-by: Alexis Laferrière <alexis.laf@xymus.net>

contrib/nitiwiki/Makefile
contrib/nitiwiki/src/wiki_edit.nit

index 57d2cda..f040751 100644 (file)
@@ -1,9 +1,12 @@
-all: nitiwiki
+all: nitiwiki bin/nitiwiki_server
 
 nitiwiki:
        mkdir -p bin
        ../../bin/nitc src/nitiwiki.nit -o bin/nitiwiki
 
+bin/nitiwiki_server: $(shell ../../bin/nitls -M src/wiki_edit.nit)
+       ../../bin/nitc -o $@ src/wiki_edit.nit
+
 check: nitiwiki
        cd tests; make
 
index 725f788..22faa25 100644 (file)
+# This file is part of NIT ( http://www.nitlanguage.org ).
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+# Web server to server generated files and modify the wiki from a web form
+module wiki_edit
+
 import nitcorn
 import markdown
+import md5
+
+intrude import wiki_html
+
+# Page for editing markdown source
+class WikiEditForm
+       super WikiArticle
+
+       # Part of the title before the name of the page
+       var title_prefix: String
+
+       # Markdown content, for previews
+       redef var md
+
+       # Custom HTML code, for forms and links
+       var html: String
+
+       init do content = (md or else "").md_to_html.to_s + html
 
-fun html_document(body: String): String do
-    return """
-        <!DOCTYPE html>
-        <head>
-            <meta charset="utf-8">
-            <title>Nitiwiki Edit</title>
-        </head>
-        <body>
-          """ + body + """
-        </body>
-        </html>"""
+       redef fun dir_href do return "edit" / href
+
+       redef fun tpl_article
+       do
+               var s = super
+               s.title = title_prefix + title
+               return s
+       end
+
+       # Fill and return a new `HttpResponse` with this page content
+       fun to_http_response: HttpResponse
+       do
+               var resp = new HttpResponse(200)
+               resp.body = tpl_page.write_to_string
+               return resp
+       end
 end
 
-class EditMarkdownAction
-    super Action
-
-    redef fun answer(http_request, turi)
-    do
-        var response = new HttpResponse(200)
-        var file_path = turi.substring(1, turi.length)
-        var md_file = new FileReader.open(file_path)
-        response.body = html_document("""
-            <form method="POST" action="/preview">
-              You may edit the file. When you are done, click on "Submit".<br/>
-              <textarea name="content" rows="30" cols="80" style="font-family: Courier">""" + md_file.read_all + """</textarea><br/>
-              <input type="submit" name="action" value="Preview">
-              <input type="submit" name="action" value="Submit">
-              <input type="hidden" name="filepath" value="""" + file_path + """">
-            </form>""")
-        md_file.close
-        return response
-    end
+# Action to serve edit forms, show previews and apply changes
+class EditAction
+       super Action
+
+       # Full public URL for the root of this wiki
+       var root_url: String
+
+       # Path to the wiki config
+       var config_file_path: String
+
+       # Configuration of the Wiki, loaded once
+       var wiki_config = new WikiConfig(config_file_path) is lazy
+
+       # Path to the root of the wiki
+       private var wiki_root: String = config_file_path.dirname is lazy
+
+       # Path to the source files
+       private var source_dir: String = (wiki_root / wiki_config.source_dir).simplify_path + "/" is lazy
+
+       # List of acceptable password to apply modifications
+       #
+       # If `null`, no password checks are applied and all modifications are accepted.
+       var passwords: nullable Collection[String]
+
+       # Reload the wiki instance with the latest changes
+       fun wiki: Nitiwiki
+       do
+               var wiki = new Nitiwiki(wiki_config)
+               wiki.parse
+               return wiki
+       end
+
+       redef fun answer(http_request, turi)
+       do
+               var action = http_request.string_arg("action")
+               var markdown = http_request.post_args.get_or_default("content", "")
+
+               var file_path = turi.strip_leading_slash
+               file_path = wiki_root / file_path
+
+               if not file_path.simplify_path.has_prefix(source_dir) then
+                       # Attempting to access a file outside the source directory
+                       var entity = new WikiEditForm(wiki, turi.strip_leading_slash,
+                               "Access denied: ", "", "<p>Target outside of the source directory</p>")
+                       return entity.to_http_response
+               end
+
+               if action == "Submit" then
+                       var passwords = passwords
+                       var password = http_request.post_args.get_or_null("password")
+                       if passwords != null and (password == null or not passwords.has(password.md5)) then
+                               # Deny modification
+                               var entity = new WikiEditForm(wiki, turi.strip_leading_slash,
+                                       "Changes rejected: ", "", "<p>Password invalid</p>")
+                               return entity.to_http_response
+                       end
+
+                       # Save markdown source
+                       markdown = markdown.replace('\r', "")
+                       markdown.write_to_file file_path
+
+                       # Update HTML files
+                       var wiki = wiki
+                       wiki.render
+
+                       var link
+                       if turi.has_prefix("/pages/") then
+                               link = root_url / turi.substring_from(7)
+                       else link = root_url / turi
+                       link = link.strip_extension(".md") + ".html"
+
+                       # Show confirmation
+                       var body = """
+<p>Your edits were recorded and the file is updated: <a href="{{{link}}}">{{{link}}}</a></p>
+"""
+                       var entity = new WikiEditForm(wiki, turi.strip_leading_slash, "Changes saved: ", "", body)
+                       return entity.to_http_response
+               else
+                       # Show edit form, and preview when requested
+
+                       # When not in a preview, use the local content of the file
+                       if action != "Preview" then markdown = file_path.to_path.read_all
+
+                       var form = """
+<form method="POST" action="/edit{{{turi}}}">
+       You may edit the file. When you are done, click on "Submit".<br/>
+       <textarea name="content" rows="30" cols="80">{{{markdown.html_escape}}}</textarea><br/>
+"""
+                       if passwords != null then form += """
+       Password: <input type="password" name="password"><br/>
+"""
+                       form += """
+       <input type="submit" name="action" value="Preview">
+       <input type="submit" name="action" value="Submit">
+</form>
+"""
+
+                       # Show processed markdown only on preview
+                       if action != "Preview" then markdown = ""
+
+                       var entity = new WikiEditForm(wiki, turi.strip_leading_slash, "Edit source: ", markdown, form)
+                       return entity.to_http_response
+               end
+       end
 end
 
-class PreviewMarkdown2HTMLAction
-    super Action
-
-    redef fun answer(http_request, turi)
-    do
-        var response = new HttpResponse(200)
-        var content = http_request.post_args["content"]
-        var action = http_request.post_args["action"]
-        var file_path = http_request.post_args["filepath"]
-        if action == "Preview" then
-            response.body = html_document("""
-                <p>
-                """ + content.md_to_html.to_s + """
-                </p>
-            """)
-        else if action == "Submit" then
-            var md_file = new FileWriter.open(file_path)
-            md_file.write(content)
-            response.body = html_document("""
-                <p>Updated file!</p>
-            """)
-            md_file.close
-        end
-        return response
-    end
+redef class String
+       private fun strip_leading_slash: String
+       do
+               if has_prefix("/") then return substring_from(1)
+               return self
+       end
 end
 
-var vh = new VirtualHost("localhost:8080")
+var config_file_path = "config.ini"
+var iface = "localhost:8080"
+var password_file_path = "passwords"
+
+# Load passwords for file
+var passwords = if password_file_path.file_exists then
+               password_file_path.to_path.read_lines
+       else null
+
+var vh = new VirtualHost(iface)
 
-# Serve Markdown editing
-vh.routes.add new Route("/edit", new EditMarkdownAction)
-vh.routes.add new Route("/preview", new PreviewMarkdown2HTMLAction)
+# Serve Markdown editing form
+var action = new EditAction("http://" + iface, config_file_path, passwords)
+vh.routes.add new Route("/edit", action)
 
-# Serve everything else with a standard `FileServer`
-vh.routes.add new Route(null, new FileServer("/var/www/"))
+# Serve the static (and generated) content
+var path_to_public_files = config_file_path.dirname / action.wiki_config.out_dir
+vh.routes.add new Route(null, new FileServer(path_to_public_files))
 
 var factory = new HttpFactory.and_libevent
 factory.config.virtual_hosts.add vh