Escape diff's output
[wikiri.git] / wikiri.cgi
blobb46bd34a017f643e4e3e8a0d16b0f340619ab208
1 #!/usr/bin/env python
2 #coding: utf8
4 # wikiri - A single-file wiki engine.
5 # Alberto Bertogli (albertito@blitiri.com.ar)
8 # Configuration section
10 # You can edit these values, or create a file named "config.py" and put them
11 # there to make updating easier. The ones in config.py take precedence.
14 # Directory where entries are stored
15 data_path = "data/"
17 # Path where templates are stored. Use an empty string for the built-in
18 # default templates. If they're not found, the built-in ones will be used.
19 templates_path = "templates/"
21 # URL to the wiki, including the name. Can be a full URL or just the path.
22 wiki_url = "/wiki/wikiri.cgi"
24 # Style sheet (CSS) URL. Can be relative or absolute. To use the built-in
25 # default, set it to wiki_url + "/style".
26 css_url = wiki_url + "/style"
28 # Wiki title.
29 title = "I like wikis"
31 # History backend. Can be one of:
32 # - "none" for no history
33 # - "git" to use git (needs git and the data path to be a repository)
34 # - "darcs" to use darcs (needs darcs and the data path to be a repository)
35 # - "auto" to select automatically (looks if the data path is a repository)
36 history = "auto"
40 # End of configuration
41 # DO *NOT* EDIT ANYTHING PAST HERE
45 import sys
46 import os
47 import errno
48 import datetime
49 import urllib
50 import cgi
52 # Load the config file, if there is one
53 try:
54 from config import *
55 except:
56 pass
59 # Find out our URL, just in case the templates need it (the default templates
60 # do not use it at all)
61 try:
62 n = os.environ['SERVER_NAME']
63 p = os.environ['SERVER_PORT']
64 s = os.environ['SCRIPT_NAME']
65 if p == '80': p = ''
66 else: p = ':' + p
67 full_url = 'http://%s%s%s' % (n, p, s)
68 except KeyError:
69 full_url = 'Not needed'
74 # Markup processing
77 import docutils.parsers.rst
78 import docutils.nodes
79 from docutils.core import publish_parts
81 def _wiki_link_role(role, rawtext, text, lineno, inliner,
82 options = {}, content = []):
84 ref = wiki_url + '/' + \
85 urllib.quote_plus(text.encode('utf8'), safe = '')
87 node = docutils.nodes.reference(rawtext, text, refuri = ref,
88 **options)
89 return [node], []
91 def content2html(content):
92 settings = {
93 'input_encoding': 'utf8',
94 'output_encoding': 'utf8',
97 docutils.parsers.rst.roles.register_canonical_role('wikilink',
98 _wiki_link_role)
99 docutils.parsers.rst.roles.DEFAULT_INTERPRETED_ROLE = 'wikilink'
101 parts = publish_parts(content,
102 settings_overrides = settings,
103 writer_name = "html")
104 return parts['body'].encode('utf8')
106 def diff2html(diff):
107 from xml.sax.saxutils import escape
109 s = '<div class="diff">'
110 for l in diff.split('\n'):
111 l = l.rstrip()
112 if l.startswith("+") and not l.startswith("+++"):
113 c = "add"
114 elif l.startswith("-") and not l.startswith("---"):
115 c = "remove"
116 elif l.startswith(" "):
117 c = "unchanged"
118 elif l.startswith("@@"):
119 c = "position"
120 elif l.startswith("diff"):
121 c = "header"
122 else:
123 c = "other"
124 s += '<span class="%s">' % c + escape(l) + '</span>\n'
125 # note there's no need to put <br/>s because the div.diff has
126 # "white-space: pre" in the css
127 s += '</div>'
128 return s
133 # Templates
136 # Default template
138 default_article_content = """\
139 title: %(artname)s
141 This page does *not* exist yet.
144 default_main_header = """
145 <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">
147 <html>
148 <head>
149 <link href="%(css_url)s" rel="stylesheet" type="text/css" />
150 <title>%(title)s</title>
151 </head>
153 <body>
155 <h1><a href="%(url)s">%(title)s</a></h1>
157 <div class="content">
160 default_main_footer = """
161 </div><p/>
162 </div>
164 </body>
165 </html>
168 default_article_header = """
169 <div class="article">
170 <h2><a href="%(url)s/%(artqname)s">%(arttitle)s</a></h2>
171 <span class="artinfo">
172 updated on %(uyear)04d-%(umonth)02d-%(uday)02d
173 %(uhour)02d:%(uminute)02d<br/>
174 </span><p/>
175 <div class="artbody">
178 default_article_footer = """
179 <p/>
180 </div>
181 <hr/><p/>
182 <div class="footer">
183 <a href="%(url)s/about">about</a> |
184 <a href="%(url)s/help">help</a> |
185 <a href="%(url)s/%(artqname)s/log">view history</a> |
186 <a href="%(url)s/%(artqname)s/edit">edit this page</a><br/>
187 </div>
188 </div>
191 default_edit_page = """
192 <h2>Edit <a href="%(url)s/%(artqname)s">%(artname)s</a></h2>
193 <p/>
195 <div class="editpage">
196 <form method="POST" action="save">
198 <label for="newtitle">Title: </label>
199 <input id="newtitle" type="text" name="newtitle"
200 value="%(arttitle)s"/><p/>
202 <textarea name="newcontent" cols="76" rows="25">
203 %(content)s
204 </textarea><p/>
206 <label for="comment">Change summary: </label>
207 <input id="comment" type="text" name="comment"
208 size="50" value="%(comment)s"/><p/>
210 <button name="submit" type="submit" value="submit">Save</button>
211 <button name="preview" type="submit" value="preview">Preview</button>
212 </form>
213 <p/>
214 </div>
216 <div class="quickhelp">
217 <h2>Quick help</h2>
219 <ul>
220 <li>Paragraphs are split by an empty line.</li>
221 <li>Lines that are underlined with dashes ("----") are a title.</li>
222 <li>Lines beginning with an "*" form a list.</li>
223 <li>To create new links, put them between backticks, <tt>`like
224 this`</tt>.</li>
225 </ul>
227 More information:
228 <a href="http://docutils.sourceforge.net/docs/user/rst/quickref.html">reST
229 quick reference</a>.<p/>
230 </div>
232 </div>
235 default_removed_page = """
236 <div class="article">
237 <h2><a href="%(url)s/%(artqname)s">%(artname)s</a></h2>
238 <div class="artbody">
239 <p/>
240 This page has been successfuly removed.<p/>
242 </div>
243 </div>
246 default_diff_header = """
247 <h2>Modification to <a href="%(url)s/%(artqname)s">%(arttitle)s</a></h2>
248 <p/>
249 This page displays the modifications performed by the selected change. Green
250 lines are additions, red lines are removals.<p/>
253 default_diff_footer = """
254 <hr/><p/>
255 <div class="footer">
256 <a href="%(url)s/about">about</a> |
257 <a href="%(url)s/help">help</a> |
258 <a href="%(url)s/%(artqname)s/log">view history</a> |
259 <a href="%(url)s/%(artqname)s/edit">edit this page</a><br/>
260 </div>
261 </div>
264 default_help_page = """
265 <div class="article">
266 <h2>Help</h2>
267 <div class="artbody">
268 <p/>
270 This wiki uses <a href="http://docutils.sf.net/rst.html">reStructuredText</a>
271 for markup.<br/>
272 Here is a quick syntax summary.<p/>
274 <ul>
275 <li>Paragraphs are split by an empty line.</li>
276 <li>Lines that are underlined with dashes ("----") are a title.</li>
277 <li>Lines beginning with an "*" form a list.</li>
278 <li>To create new links, put them between backticks, <tt>`like
279 this`</tt>.</li>
280 </ul>
282 If you want more information, see the
283 <a href="http://docutils.sourceforge.net/docs/user/rst/quickref.html">reST
284 quick reference</a>.<p/>
286 </div>
287 </div>
290 default_about_page = """
291 <div class="article">
292 <h2>About</h2>
293 <div class="artbody">
294 <p/>
296 This wiki is powered by
297 <a href="http://blitiri.com.ar/p/misc.html">wikiri</a>, a single-file
298 blog engine written in <a href="http://python.org">Python</a>, and uses
299 <a href="http://docutils.sf.net/rst.html">reStructuredText</a>
300 for markup.<p/>
302 If you have any questions or comments, please send an email to
303 <a href="mailto:albertito@blitiri.com.ar">Alberto Bertogli</a>.<p/>
305 </div>
306 </div>
309 default_log_header = """
310 <h2>Change history for <a href="%(url)s/%(artqname)s">%(artname)s</a></h2>
311 <p/>
313 <table class="log">
316 default_log_entry = """
317 <tr>
318 <td class="date">
319 %(date_y)04d-%(date_m)02d-%(date_d)02d
320 %(date_H)02d:%(date_M)02d:%(date_S)02d
321 </td>
322 <td class="summary">%(summary)s</td>
323 <td class="links">
324 <a href="%(url)s/logview/%(commitid)s/%(artqname)s">view</a> |
325 <a href="%(url)s/restore/%(commitid)s/%(artqname)s">restore</a> |
326 <a href="%(url)s/diff/%(commitid)s/%(artqname)s">diff</a>
327 </td>
328 </tr>
331 default_log_footer = """
332 </table>
335 # Default CSS
336 default_css = """
337 body {
338 font-family: sans-serif;
341 div.content {
342 width: 60%;
345 h1 {
346 font-size: x-large;
347 border-bottom: 2px solid #99F;
348 width: 65%;
349 margin-bottom: 1em;
352 h2 {
353 font-size: large;
354 font-weight: bold;
355 margin-bottom: 1pt;
356 border-bottom: 1px solid #99C;
359 h1 a, h2 a {
360 text-decoration: none;
361 color: black;
364 span.artinfo {
365 font-size: x-small;
368 span.artinfo a {
369 text-decoration: none;
370 color: #339;
373 span.artinfo a:hover {
374 text-decoration: none;
375 color: blue;
378 div.artbody {
379 margin-left: 1em;
382 div.article {
383 margin-bottom: 2em;
386 hr {
387 /* hack to properly align the hr */
388 text-align: left;
389 margin: 0 auto 0 0;
391 height: 2px;
392 border: 0;
393 background-color: #99F;
394 width: 60%;
397 div.footer {
398 font-size: small;
401 div.footer a {
402 text-decoration: none;
405 table.log {
406 border-right: 1px solid #6666cc;
407 border-collapse: collapse;
408 margin-left: 1em;
411 table.log td {
412 border-left: 1px solid #6666cc;
413 padding-right: 1em;
414 padding-left: 1em;
417 table.log td.date {
418 white-space: nowrap;
421 table.log td.summary {
425 table.log td.links {
426 white-space: nowrap;
427 font-size: small;
430 /* Articles are enclosed in <div class="section"> */
431 div.section h1 {
432 font-size: medium;
433 font-weight: bold;
434 width: 100%;
435 margin-bottom: 1pt;
436 border-bottom: 1px dotted #99C;
439 div.section h2 {
440 font-size: small;
441 font-weight: bold;
442 width: 75%;
443 margin-bottom: 1pt;
444 border-bottom: 1px dotted #DDD;
447 /* diff */
448 div.diff {
449 font-family: monospace;
450 white-space: pre;
451 margin: 0;
452 padding: 0;
455 div.diff span.add {
456 color: #090;
459 div.diff span.remove {
460 color: #900;
463 div.diff span.unchanged {
466 div.diff span.position {
467 background-color: #E5E5FF;
470 div.diff span.header {
471 background-color: #CCF;
472 font-weight: bold;
475 div.diff span.other {
480 class Templates (object):
481 def __init__(self):
482 self.tpath = templates_path
483 now = datetime.datetime.now()
485 self.vars = {
486 'css_url': css_url,
487 'title': title,
488 'url': wiki_url,
489 'fullurl': full_url,
490 'year': now.year,
491 'month': now.month,
492 'day': now.day,
495 def art_vars(self, article):
496 avars = self.vars.copy()
497 avars.update( {
498 'arttitle': article.title,
499 'artname': article.name,
500 'artqname': article.qname,
501 'updated': article.updated.isoformat(' '),
503 'uyear': article.updated.year,
504 'umonth': article.updated.month,
505 'uday': article.updated.day,
506 'uhour': article.updated.hour,
507 'uminute': article.updated.minute,
508 'usecond': article.updated.second,
510 return avars
512 def get_main_header(self):
513 p = self.tpath + '/header.html'
514 if os.path.isfile(p):
515 return open(p).read() % self.vars
516 return default_main_header % self.vars
518 def get_default_article_content(self, artname):
519 avars = self.vars.copy()
520 avars.update( {
521 'artname': artname,
522 'artqname': urllib.quote_plus(artname, safe = ''),
524 p = self.tpath + '/default_article_content.wiki'
525 if os.path.isfile(p):
526 return open(p).read() % avars
527 return default_article_content % avars
529 def get_main_footer(self):
530 p = self.tpath + '/footer.html'
531 if os.path.isfile(p):
532 return open(p).read() % self.vars
533 return default_main_footer % self.vars
535 def get_article_header(self, article):
536 avars = self.art_vars(article)
537 p = self.tpath + '/art_header.html'
538 if os.path.isfile(p):
539 return open(p).read() % avars
540 return default_article_header % avars
542 def get_article_footer(self, article):
543 avars = self.art_vars(article)
544 p = self.tpath + '/art_footer.html'
545 if os.path.isfile(p):
546 return open(p).read() % avars
547 return default_article_footer % avars
549 def get_edit_page(self, article, comment):
550 avars = self.art_vars(article)
551 avars.update( {
552 'content': article.raw_content,
553 'comment': comment,
555 p = self.tpath + '/edit_page.html'
556 if os.path.isfile(p):
557 return open(p).read() % avars
558 return default_edit_page % avars
560 def get_removed_page(self, artname):
561 avars = self.vars.copy()
562 avars.update( {
563 'artname': artname,
564 'artqname': urllib.quote_plus(artname, safe = ''),
566 p = self.tpath + '/removed_page.html'
567 if os.path.isfile(p):
568 return open(p).read() % avars
569 return default_removed_page % avars
571 def get_diff_header(self, article, commitid):
572 avars = self.art_vars(article)
573 avars.update( {
574 'commitid': commitid,
576 p = self.tpath + '/diff_header.html'
577 if os.path.isfile(p):
578 return open(p).read() % avars
579 return default_diff_header % avars
581 def get_diff_footer(self, article, commitid):
582 avars = self.art_vars(article)
583 avars.update( {
584 'commitid': commitid,
586 p = self.tpath + '/diff_footer.html'
587 if os.path.isfile(p):
588 return open(p).read() % avars
589 return default_diff_footer % avars
591 def get_help_page(self):
592 p = self.tpath + '/help_page.html'
593 if os.path.isfile(p):
594 return open(p).read() % self.vars
595 return default_help_page % self.vars
597 def get_about_page(self):
598 p = self.tpath + '/about_page.html'
599 if os.path.isfile(p):
600 return open(p).read() % self.vars
601 return default_about_page % self.vars
603 def get_log_header(self, artname):
604 avars = self.vars.copy()
605 avars.update( {
606 'artname': artname,
607 'artqname': urllib.quote_plus(artname, safe = ''),
609 p = self.tpath + '/log_header.html'
610 if os.path.isfile(p):
611 return open(p).read() % avars
612 return default_log_header % avars
614 def get_log_entry(self, artname, commit):
615 avars = self.vars.copy()
616 avars.update( {
617 'artname': artname,
618 'artqname': urllib.quote_plus(artname, safe = ''),
619 'summary': commit['msg'],
620 'commitid': commit['commit'],
621 'date_y': commit['atime'].year,
622 'date_m': commit['atime'].month,
623 'date_d': commit['atime'].day,
624 'date_H': commit['atime'].hour,
625 'date_M': commit['atime'].minute,
626 'date_S': commit['atime'].second,
628 p = self.tpath + '/log_entry.html'
629 if os.path.isfile(p):
630 return open(p).read() % avars
631 return default_log_entry % avars
633 def get_log_footer(self, artname):
634 avars = self.vars.copy()
635 avars.update( {
636 'artname': artname,
637 'artqname': urllib.quote_plus(artname, safe = ''),
639 p = self.tpath + '/log_footer.html'
640 if os.path.isfile(p):
641 return open(p).read() % avars
642 return default_log_footer % avars
647 # Article handling
650 class Article (object):
651 def __init__(self, name, title = None, content = None,
652 has_header = True):
653 self.name = name
654 self.qname = urllib.quote_plus(name, safe = "")
655 self.updated = None
657 self.loaded = False
659 self.preloaded_title = title
660 self.preloaded_content = content
661 self.has_header = has_header
663 # loaded on demand
664 self.attrs = {}
665 self._raw_content = ''
667 def get_raw_content(self):
668 if not self.loaded:
669 self.load()
670 return self._raw_content
671 raw_content = property(fget = get_raw_content)
673 def get_title(self):
674 if not self.loaded:
675 self.load()
676 # use the name by default
677 return self.attrs.get('title', self.name)
678 title = property(fget = get_title)
680 def load(self):
681 try:
682 if self.preloaded_content:
683 raw = self.preloaded_content
684 raw = [ s + '\n' for s in raw.split('\n') ]
685 self.updated = datetime.datetime.now()
686 else:
687 fd = open(data_path + '/' + self.qname)
688 raw = fd.readlines()
689 stat = os.fstat(fd.fileno())
690 self.updated = datetime.datetime.fromtimestamp(stat.st_mtime)
691 except:
692 t = Templates()
693 raw = t.get_default_article_content(self.name)
694 raw = [ s + '\n' for s in raw.split('\n') ]
695 self.updated = datetime.datetime.now()
697 hdr_lines = 0
698 if self.has_header:
699 for l in raw:
700 if ':' in l:
701 name, value = l.split(':', 1)
702 name = name.lower().strip()
703 self.attrs[name] = value.strip()
704 hdr_lines += 1
706 elif l == '\n' or l == '\r\n':
707 # end of header
708 hdr_lines += 1
709 break
711 self._raw_content = ''.join(raw[hdr_lines:])
712 self.loaded = True
714 if self.preloaded_title:
715 self.attrs['title'] = self.preloaded_title
717 def save(self, newtitle, newcontent, raw = False):
718 fd = open(data_path + '/' + self.qname, 'w+')
719 if raw:
720 fd.write(newcontent)
721 else:
722 fd.write('title: %s\n\n' % newtitle)
723 fd.write(newcontent.rstrip() + '\n')
724 fd.close()
726 # invalidate our information
727 self.loaded = False
729 def remove(self):
730 try:
731 os.unlink(data_path + '/' + self.qname)
732 except OSError, errno.ENOENT:
733 pass
735 def to_html(self):
736 return content2html(self.raw_content)
740 # History backends
742 # At the moment we support none, git and darcs
744 class HistoryError (Exception):
745 pass
747 class History:
748 def __init__(self):
749 if history == 'auto':
750 if os.path.isdir(data_path + '.git'):
751 self.be = GitBackend(data_path)
752 elif os.path.isdir(data_path + '_darcs'):
753 self.be = DarcsBackend(data_path)
754 else:
755 self.be = NoneBackend(data_path)
756 elif history == 'git':
757 self.be = GitBackend(data_path)
758 elif history == 'darcs':
759 self.be = DarcsBackend(data_path)
760 else:
761 self.be = NoneBackend(data_path)
763 def commit(self, msg, author = 'Wikiri <somebody@wikiri>'):
764 self.be.commit(msg, author = author)
766 def log(self, fname):
767 # log() yields commits of the form:
769 # 'commit': commit id
770 # 'author': author of the commit
771 # 'committer': committer
772 # 'atime': time of the change itself
773 # 'ctime': time of the commit
774 # 'msg': commit msg (one line)
776 return self.be.log(file = fname)
778 def add(self, *files):
779 return self.be.add(*files)
781 def remove(self, *files):
782 return self.be.remove(*files)
784 def get_content(self, fname, cid):
785 return self.be.get_content(fname, cid)
787 def get_commit(self, cid):
788 return self.be.get_commit(cid)
790 def get_diff(self, cid):
791 # get_diff() returns the diff in unified format
792 return self.be.get_diff(cid)
794 class NoneBackend:
795 def __init__(self, repopath):
796 pass
798 def commit(self, *args, **kwargs):
799 pass
801 def log(self, *args, **kwargs):
802 return ()
804 def add(self, *files):
805 pass
807 def remove(self, *files):
808 pass
810 def get_content(self, fname, cid):
811 return "Not supported."
813 def get_commit(self, cid):
814 return { 'commit': cid }
816 def get_diff(self, cid):
817 return ""
819 class GitBackend:
820 def __init__(self, repopath):
821 self.repo = repopath
822 self.prevdir = None
824 def cdrepo(self):
825 self.prevdir = os.getcwd()
826 os.chdir(self.repo)
828 def cdback(self):
829 os.chdir(self.prevdir)
831 def git(self, *args):
832 # delay the import to avoid the hit on a regular page view
833 import subprocess
834 self.cdrepo()
835 cmd = subprocess.Popen( ['git'] + list(args),
836 stdin = subprocess.PIPE,
837 stdout = subprocess.PIPE,
838 stderr = sys.stderr )
839 self.cdback()
840 return cmd
842 def gitq(self, *args):
843 cmd = self.git(*args)
844 stdout, stderr = cmd.communicate()
845 return cmd.returncode
847 def commit(self, msg, author = None):
848 if not author:
849 author = "Unknown <unknown@example.com>"
851 # see if we have something to commit; if not, just return
852 self.gitq('update-index', '--refresh')
853 r = self.gitq('diff-index', '--exit-code', '--quiet', 'HEAD')
854 if r == 0:
855 return
857 r = self.gitq('commit',
858 '-m', msg,
859 '--author', author)
860 if r != 0:
861 raise HistoryError, r
863 def log(self, file = None, files = None):
864 if not files:
865 files = []
866 if file:
867 files.append(file)
869 cmd = self.git("rev-list",
870 "--all", "--pretty=raw",
871 "HEAD", "--", *files)
872 cmd.stdin.close()
874 commit = { 'msg': '' }
875 in_hdr = True
876 l = cmd.stdout.readline()
877 while l:
878 if l != '\n' and not l.startswith(' '):
879 name, value = l[:-1].split(' ', 1)
880 if in_hdr:
881 commit[name] = value
882 else:
883 # the previous commit has ended
884 _add_times(commit)
885 yield commit
886 commit = { 'msg': '' }
887 in_hdr = True
889 # continue reusing the line
890 continue
891 else:
892 if in_hdr:
893 in_hdr = False
894 if l.startswith(' '):
895 l = l[4:]
896 commit['msg'] += l
898 l = cmd.stdout.readline()
900 # the last commit, if there is one
901 if not in_hdr:
902 _add_times(commit)
903 yield commit
905 cmd.wait()
906 if cmd.returncode != 0:
907 raise HistoryError, cmd.returncode
909 def add(self, *files):
910 r = self.gitq('add', "--", *files)
911 if r != 0:
912 raise HistoryError, r
914 def remove(self, *files):
915 r = self.gitq('rm', '-f', '--', *files)
916 if r != 0:
917 raise HistoryError, r
919 def get_content(self, fname, commitid):
920 cmd = self.git("show", "%s:%s" % (commitid, fname))
921 content = cmd.stdout.read()
922 cmd.wait()
923 return content
925 def get_commit(self, cid):
926 cmd = self.git("rev-list", "-n1", "--pretty=raw", cid)
927 out = cmd.stdout.readlines()
928 cmd.wait()
929 commit = { 'msg': '' }
930 for l in out:
931 if l != '\n' and not l.startswith(' '):
932 name, value = l[:-1].split(' ', 1)
933 commit[name] = value
934 else:
935 commit['msg'] += l
936 _add_times(commit)
937 return commit
939 def get_diff(self, cid):
940 cmd = self.git("diff", cid + "^.." + cid)
941 out = cmd.stdout.read()
942 cmd.wait()
943 return out
945 def _add_times(commit):
946 if 'author' in commit:
947 author, epoch, tz = commit['author'].rsplit(' ', 2)
948 epoch = float(epoch)
949 commit['author'] = author
950 commit['atime'] = datetime.datetime.fromtimestamp(epoch)
952 if 'committer' in commit:
953 committer, epoch, tz = commit['committer'].rsplit(' ', 2)
954 epoch = float(epoch)
955 commit['committer'] = committer
956 commit['ctime'] = datetime.datetime.fromtimestamp(epoch)
958 class DarcsBackend:
959 def __init__(self, repopath):
960 self.repo = repopath
961 self.prevdir = None
963 def cdrepo(self):
964 self.prevdir = os.getcwd()
965 os.chdir(self.repo)
967 def cdback(self):
968 os.chdir(self.prevdir)
970 def darcs(self, *args):
971 # delay the import to avoid the hit on a regular page view
972 import subprocess
973 self.cdrepo()
974 cmd = subprocess.Popen( ['darcs'] + list(args),
975 stdin = subprocess.PIPE,
976 stdout = subprocess.PIPE,
977 stderr = sys.stderr )
978 self.cdback()
979 return cmd
981 def darcsq(self, *args):
982 cmd = self.darcs(*args)
983 stdout, stderr = cmd.communicate()
984 return cmd.returncode
986 def commit(self, msg, author = None):
987 if not author:
988 author = "Unknown <unknown@example.com>"
990 # see if we have something to commit; if not, just return
991 if self.darcsq('whatsnew') != 0:
992 return
994 r = self.darcsq('record',
995 '-a',
996 '-m', msg,
997 '-A', author)
998 if r != 0:
999 raise HistoryError, r
1001 def log(self, file = None, files = None):
1002 import xml.dom.minidom as minidom
1004 if not files:
1005 files = []
1006 if file:
1007 files.append(file)
1009 cmd = self.darcs("changes",
1010 "--xml-output",
1011 "--", *files)
1012 cmd.stdin.close()
1014 xml = minidom.parse(cmd.stdout)
1016 cmd.wait()
1017 if cmd.returncode != 0:
1018 raise HistoryError, cmd.returncode
1020 for p in xml.getElementsByTagName('patch'):
1021 # ignore patches not directly inside <changelog> (they
1022 # can be, for instance, inside <created_as>, which we
1023 # want to ignore)
1024 if p.parentNode.nodeName != 'changelog':
1025 continue
1027 cid = p.getAttribute("hash")
1028 author = p.getAttribute('author')
1029 atime = p.getAttribute('date')
1030 atime = datetime.datetime.strptime(atime,
1031 "%Y%m%d%H%M%S")
1032 msg = p.getElementsByTagName('name')[0]
1033 msg = msg.childNodes[0].data
1034 msg = msg.split('\n')[0].strip()
1036 commit = {
1037 'commit': cid.encode('utf8'),
1038 'author': author.encode('utf8'),
1039 'committer': author.encode('utf8'),
1040 'atime': atime,
1041 'ctime': atime,
1042 'msg': msg.encode('utf8'),
1044 yield commit
1046 def add(self, *files):
1047 r = self.darcsq('add', "--", *files)
1048 # 0 means success, 2 means the file was already there (which
1049 # is ok because we always add the files)
1050 if r != 0 and r != 2:
1051 raise HistoryError, r
1053 def remove(self, *files):
1054 r = self.darcsq('remove', '--', *files)
1055 if r != 0:
1056 raise HistoryError, r
1058 def get_content(self, fname, commitid):
1059 cmd = self.darcs("show", "contents",
1060 "--match", "hash %s" % commitid,
1061 "--", fname)
1062 content = cmd.stdout.read()
1063 cmd.wait()
1064 return content
1066 def get_commit(self, cid):
1067 import xml.dom.minidom as minidom
1069 cmd = self.darcs("changes",
1070 "--xml-output",
1071 "--match", "hash %s" % cid)
1072 cmd.stdin.close()
1074 xml = minidom.parse(cmd.stdout)
1076 cmd.wait()
1077 if cmd.returncode != 0:
1078 raise HistoryError, cmd.returncode
1080 try:
1081 p = xml.getElementsByTagName('patch')[0]
1082 except IndexError:
1083 raise HistoryError, "not such patch"
1085 cid = p.getAttribute("hash")
1086 author = p.getAttribute('author')
1087 atime = p.getAttribute('date')
1088 atime = datetime.datetime.strptime(atime,
1089 "%Y%m%d%H%M%S")
1090 msg = p.getElementsByTagName('name')[0]
1091 msg = msg.childNodes[0].data
1092 msg = msg.split('\n')[0].strip()
1094 commit = {
1095 'commit': cid.encode('utf8'),
1096 'author': author.encode('utf8'),
1097 'committer': author.encode('utf8'),
1098 'atime': atime,
1099 'ctime': atime,
1100 'msg': msg.encode('utf8'),
1102 return commit
1104 def get_diff(self, cid):
1105 # TODO
1106 return ""
1111 # Main
1114 def render_article(art):
1115 template = Templates()
1116 print 'Content-type: text/html; charset=utf-8\n'
1117 print template.get_main_header()
1118 print template.get_article_header(art)
1119 print art.to_html()
1120 print template.get_article_footer(art)
1121 print template.get_main_footer()
1123 def render_edit(art, preview = False, comment = ""):
1124 template = Templates()
1125 print 'Content-type: text/html; charset=utf-8\n'
1126 print template.get_main_header()
1127 if preview:
1128 print template.get_article_header(art)
1129 print art.to_html()
1130 print template.get_article_footer(art)
1131 print template.get_edit_page(art, comment)
1132 print template.get_main_footer()
1134 def render_removed(artname):
1135 template = Templates()
1136 print 'Content-type: text/html; charset=utf-8\n'
1137 print template.get_main_header()
1138 print template.get_removed_page(artname)
1139 print template.get_main_footer()
1141 def render_log(artname, log):
1142 template = Templates()
1143 print 'Content-type: text/html; charset=utf-8\n'
1144 print template.get_main_header()
1145 print template.get_log_header(artname)
1146 for commit in log:
1147 print template.get_log_entry(artname, commit)
1148 print template.get_log_footer(artname)
1149 print template.get_main_footer()
1151 def render_diff(article, cid, diff):
1152 template = Templates()
1153 print 'Content-type: text/html; charset=utf-8\n'
1154 print template.get_main_header()
1155 print template.get_diff_header(article, cid)
1156 print diff2html(diff)
1157 print template.get_diff_footer(article, cid)
1158 print template.get_main_footer()
1160 def render_help():
1161 template = Templates()
1162 print 'Content-type: text/html; charset=utf-8\n'
1163 print template.get_main_header()
1164 print template.get_help_page()
1165 print template.get_main_footer()
1167 def render_about():
1168 template = Templates()
1169 print 'Content-type: text/html; charset=utf-8\n'
1170 print template.get_main_header()
1171 print template.get_about_page()
1172 print template.get_main_footer()
1174 def render_style():
1175 print 'Content-type: text/css\n'
1176 print default_css
1178 def redirect(artname):
1179 print 'Status: 303 See Other\r\n',
1180 print 'Location: %s/%s\r\n' % (wiki_url, artname),
1181 print
1183 def handle_cgi():
1184 import cgitb; cgitb.enable()
1186 form = cgi.FieldStorage()
1188 edit = False
1189 save = False
1190 log = False
1191 artname = 'index'
1193 newcontent = form.getfirst("newcontent", '')
1194 newtitle = form.getfirst("newtitle", '').strip()
1195 preview = form.getfirst("preview", '').strip()
1196 comment = form.getfirst("comment", '').strip()
1198 remoteip = os.environ.get("REMOTE_ADDR", "unknownip")
1199 author = "Somebody <%s@wikiri>" % remoteip
1201 if os.environ.has_key('PATH_INFO'):
1202 path_info = os.environ['PATH_INFO']
1203 path_info = os.path.normpath(path_info)
1205 edit = path_info.endswith('/edit')
1206 save = path_info.endswith('/save')
1207 log = path_info.endswith('/log')
1209 if edit or save or log:
1210 artname = path_info[1:].rsplit('/', 1)[0]
1211 else:
1212 artname = path_info[1:]
1214 if artname == '' or artname == '/':
1215 artname = 'index'
1217 if save and not os.environ.get('REQUEST_METHOD', 'GET') == 'POST':
1218 # only allow saves if the request is a post to prevent people
1219 # from accidentally performing a GET .../save, which would
1220 # result in an empty save, with the following page removal
1221 save = False
1223 artname = urllib.unquote_plus(artname)
1225 if artname == 'style':
1226 render_style()
1227 elif artname == 'help':
1228 render_help()
1229 elif artname == 'about':
1230 render_about()
1231 elif edit or (save and preview):
1232 if preview:
1233 art = Article(artname,
1234 title = newtitle,
1235 content = newcontent,
1236 has_header = False)
1237 render_edit(art, preview = True, comment = comment)
1238 else:
1239 render_edit(Article(artname))
1240 elif save:
1241 if not comment:
1242 comment = "No comment"
1243 h = History()
1244 a = Article(artname)
1245 if newcontent.strip():
1246 a.save(newtitle, newcontent)
1247 h.add(a.qname)
1248 h.commit(msg = comment, author = author)
1249 redirect(artname)
1250 else:
1251 a.remove()
1252 h.remove(a.qname)
1253 h.commit(msg = comment, author = author)
1254 redirect(artname)
1255 elif log:
1256 a = Article(artname)
1257 render_log(a.name, History().log(a.qname))
1258 elif artname.startswith("logview/"):
1259 unused, cid, artname = artname.split('/', 2)
1260 artname = urllib.unquote_plus(artname)
1261 oldcontent = History().get_content(Article(artname).qname, cid)
1262 render_article(Article(artname, content = oldcontent))
1263 elif artname.startswith("restore/"):
1264 unused, cid, artname = artname.split('/', 2)
1265 artname = urllib.unquote_plus(artname)
1267 h = History()
1268 a = Article(artname)
1269 oldcontent = h.get_content(a.qname, cid)
1271 a.save(None, oldcontent, raw = True)
1272 h.add(a.qname)
1273 ctime = h.get_commit(cid)['atime'].strftime("%Y-%m-%d %H:%M:%S")
1274 h.commit(msg = 'Restored ' + ctime, author = author)
1276 redirect(artname)
1277 elif artname.startswith("diff/"):
1278 unused, cid, artname = artname.split('/', 2)
1279 artname = urllib.unquote_plus(artname)
1280 diff = History().get_diff(cid)
1281 render_diff(Article(artname), cid, diff)
1282 else:
1283 render_article(Article(artname))
1286 def handle_cmd():
1287 print "This is a CGI application."
1288 print "It only runs inside a web server."
1289 return 1
1292 if os.environ.has_key('GATEWAY_INTERFACE'):
1293 handle_cgi()
1294 else:
1295 sys.exit(handle_cmd())