7dd60122b16d6ab286688a92d2f6177885cf0d71
[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 var abs_file_path = file_path.to_absolute_path
98 var abs_source_dir = source_dir.to_absolute_path
99
100 if not abs_file_path.has_prefix(abs_source_dir) then
101 # Attempting to access a file outside the source directory
102 var entity = new WikiEditForm(wiki, turi.strip_leading_slash,
103 "Access denied: ", "", "<p>Target outside of the source directory</p>")
104 return entity.to_http_response
105 end
106
107 if action == "Submit" then
108 var passwords = passwords
109 var password = http_request.post_args.get_or_null("password")
110 if passwords != null and (password == null or not passwords.has(password.md5)) then
111 # Deny modification
112 var entity = new WikiEditForm(wiki, turi.strip_leading_slash,
113 "Changes rejected: ", "", "<p>Password invalid</p>")
114 return entity.to_http_response
115 end
116
117 # Save markdown source
118 markdown = markdown.replace('\r', "")
119 markdown.write_to_file file_path
120
121 # Update HTML files
122 var wiki = wiki
123 wiki.render
124
125 var link
126 if turi.has_prefix("/pages/") then
127 link = root_url / turi.substring_from(7)
128 else link = root_url / turi
129 link = link.strip_extension(".md") + ".html"
130
131 # Show confirmation
132 var body = """
133 <p>Your edits were recorded and the file is updated: <a href="{{{link}}}">{{{link}}}</a></p>
134 """
135 var entity = new WikiEditForm(wiki, turi.strip_leading_slash, "Changes saved: ", "", body)
136 return entity.to_http_response
137 else
138 # Show edit form, and preview when requested
139
140 # When not in a preview, use the local content of the file
141 if action != "Preview" then markdown = file_path.to_path.read_all
142
143 var form = """
144 <form method="POST" action="/edit{{{turi}}}">
145 You may edit the file. When you are done, click on "Submit".<br/>
146 <textarea name="content" rows="30" cols="80">{{{markdown.html_escape}}}</textarea><br/>
147 """
148 if passwords != null then form += """
149 Password: <input type="password" name="password"><br/>
150 """
151 form += """
152 <input type="submit" name="action" value="Preview">
153 <input type="submit" name="action" value="Submit">
154 </form>
155 """
156
157 # Show processed markdown only on preview
158 if action != "Preview" then markdown = ""
159
160 var entity = new WikiEditForm(wiki, turi.strip_leading_slash, "Edit source: ", markdown, form)
161 return entity.to_http_response
162 end
163 end
164 end
165
166 redef class String
167 private fun strip_leading_slash: String
168 do
169 if has_prefix("/") then return substring_from(1)
170 return self
171 end
172
173 private fun to_absolute_path: String
174 do
175 return (getcwd / self).simplify_path
176 end
177 end
178
179 var config_file_path = "config.ini"
180 var iface = "localhost:8080"
181 var password_file_path = "passwords"
182
183 # Load passwords for file
184 var passwords = if password_file_path.file_exists then
185 password_file_path.to_path.read_lines
186 else null
187
188 var vh = new VirtualHost(iface)
189
190 # Serve Markdown editing form
191 var action = new EditAction("http://" + iface, config_file_path, passwords)
192 vh.routes.add new Route("/edit", action)
193
194 # Serve the static (and generated) content
195 var path_to_public_files = config_file_path.dirname / action.wiki_config.out_dir
196 vh.routes.add new Route(null, new FileServer(path_to_public_files))
197
198 var factory = new HttpFactory.and_libevent
199 factory.config.virtual_hosts.add vh
200 factory.run