contrib/nitiwiki: complete the nitcorn server to modify a nitiwiki
[nit.git] / contrib / nitiwiki / src / wiki_edit.nit
1 # This file is part of NIT ( http://www.nitlanguage.org ).
2 #
3 # Licensed under the Apache License, Version 2.0 (the "License");
4 # you may not use this file except in compliance with the License.
5 # You may obtain a copy of the License at
6 #
7 # http://www.apache.org/licenses/LICENSE-2.0
8 #
9 # Unless required by applicable law or agreed to in writing, software
10 # distributed under the License is distributed on an "AS IS" BASIS,
11 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 # See the License for the specific language governing permissions and
13 # limitations under the License.
14
15 # Web server to server generated files and modify the wiki from a web form
16 module wiki_edit
17
18 import nitcorn
19 import markdown
20 import md5
21
22 intrude import wiki_html
23
24 # Page for editing markdown source
25 class WikiEditForm
26 super WikiArticle
27
28 # Part of the title before the name of the page
29 var title_prefix: String
30
31 # Markdown content, for previews
32 redef var md
33
34 # Custom HTML code, for forms and links
35 var html: String
36
37 init do content = (md or else "").md_to_html.to_s + html
38
39 redef fun dir_href do return "edit" / href
40
41 redef fun tpl_article
42 do
43 var s = super
44 s.title = title_prefix + title
45 return s
46 end
47
48 # Fill and return a new `HttpResponse` with this page content
49 fun to_http_response: HttpResponse
50 do
51 var resp = new HttpResponse(200)
52 resp.body = tpl_page.write_to_string
53 return resp
54 end
55 end
56
57 # Action to serve edit forms, show previews and apply changes
58 class EditAction
59 super Action
60
61 # Full public URL for the root of this wiki
62 var root_url: String
63
64 # Path to the wiki config
65 var config_file_path: String
66
67 # Configuration of the Wiki, loaded once
68 var wiki_config = new WikiConfig(config_file_path) is lazy
69
70 # Path to the root of the wiki
71 private var wiki_root: String = config_file_path.dirname is lazy
72
73 # Path to the source files
74 private var source_dir: String = (wiki_root / wiki_config.source_dir).simplify_path + "/" is lazy
75
76 # List of acceptable password to apply modifications
77 #
78 # If `null`, no password checks are applied and all modifications are accepted.
79 var passwords: nullable Collection[String]
80
81 # Reload the wiki instance with the latest changes
82 fun wiki: Nitiwiki
83 do
84 var wiki = new Nitiwiki(wiki_config)
85 wiki.parse
86 return wiki
87 end
88
89 redef fun answer(http_request, turi)
90 do
91 var action = http_request.string_arg("action")
92 var markdown = http_request.post_args.get_or_default("content", "")
93
94 var file_path = turi.strip_leading_slash
95 file_path = wiki_root / file_path
96
97 if not file_path.simplify_path.has_prefix(source_dir) then
98 # Attempting to access a file outside the source directory
99 var entity = new WikiEditForm(wiki, turi.strip_leading_slash,
100 "Access denied: ", "", "<p>Target outside of the source directory</p>")
101 return entity.to_http_response
102 end
103
104 if action == "Submit" then
105 var passwords = passwords
106 var password = http_request.post_args.get_or_null("password")
107 if passwords != null and (password == null or not passwords.has(password.md5)) then
108 # Deny modification
109 var entity = new WikiEditForm(wiki, turi.strip_leading_slash,
110 "Changes rejected: ", "", "<p>Password invalid</p>")
111 return entity.to_http_response
112 end
113
114 # Save markdown source
115 markdown = markdown.replace('\r', "")
116 markdown.write_to_file file_path
117
118 # Update HTML files
119 var wiki = wiki
120 wiki.render
121
122 var link
123 if turi.has_prefix("/pages/") then
124 link = root_url / turi.substring_from(7)
125 else link = root_url / turi
126 link = link.strip_extension(".md") + ".html"
127
128 # Show confirmation
129 var body = """
130 <p>Your edits were recorded and the file is updated: <a href="{{{link}}}">{{{link}}}</a></p>
131 """
132 var entity = new WikiEditForm(wiki, turi.strip_leading_slash, "Changes saved: ", "", body)
133 return entity.to_http_response
134 else
135 # Show edit form, and preview when requested
136
137 # When not in a preview, use the local content of the file
138 if action != "Preview" then markdown = file_path.to_path.read_all
139
140 var form = """
141 <form method="POST" action="/edit{{{turi}}}">
142 You may edit the file. When you are done, click on "Submit".<br/>
143 <textarea name="content" rows="30" cols="80">{{{markdown.html_escape}}}</textarea><br/>
144 """
145 if passwords != null then form += """
146 Password: <input type="password" name="password"><br/>
147 """
148 form += """
149 <input type="submit" name="action" value="Preview">
150 <input type="submit" name="action" value="Submit">
151 </form>
152 """
153
154 # Show processed markdown only on preview
155 if action != "Preview" then markdown = ""
156
157 var entity = new WikiEditForm(wiki, turi.strip_leading_slash, "Edit source: ", markdown, form)
158 return entity.to_http_response
159 end
160 end
161 end
162
163 redef class String
164 private fun strip_leading_slash: String
165 do
166 if has_prefix("/") then return substring_from(1)
167 return self
168 end
169 end
170
171 var config_file_path = "config.ini"
172 var iface = "localhost:8080"
173 var password_file_path = "passwords"
174
175 # Load passwords for file
176 var passwords = if password_file_path.file_exists then
177 password_file_path.to_path.read_lines
178 else null
179
180 var vh = new VirtualHost(iface)
181
182 # Serve Markdown editing form
183 var action = new EditAction("http://" + iface, config_file_path, passwords)
184 vh.routes.add new Route("/edit", action)
185
186 # Serve the static (and generated) content
187 var path_to_public_files = config_file_path.dirname / action.wiki_config.out_dir
188 vh.routes.add new Route(null, new FileServer(path_to_public_files))
189
190 var factory = new HttpFactory.and_libevent
191 factory.config.virtual_hosts.add vh
192 factory.run