]> git.llucax.com Git - software/blitiri.git/blob - blitiri.cgi
Add form error reporting to online comments
[software/blitiri.git] / blitiri.cgi
1 #!/usr/bin/env python
2 #coding: utf8
3
4 # blitiri - A single-file blog engine.
5 # Alberto Bertogli (albertito@gmail.com)
6
7 #
8 # Configuration section
9 #
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.
12 #
13
14 # Directory where entries are stored
15 data_path = "/tmp/blog/data"
16
17 # Are comments allowed? (if False, comments_path option is not used)
18 enable_comments = False
19
20 # Directory where comments are stored (must be writeable by the web server)
21 comments_path = "/tmp/blog/comments"
22
23 # Path where templates are stored. Use an empty string for the built-in
24 # default templates. If they're not found, the built-in ones will be used.
25 templates_path = "/tmp/blog/templates"
26
27 # URL to the blog, including the name. Can be a full URL or just the path.
28 blog_url = "/blog/blitiri.cgi"
29
30 # Style sheet (CSS) URL. Can be relative or absolute. To use the built-in
31 # default, set it to blog_url + "/style".
32 css_url = blog_url + "/style"
33
34 # Blog title
35 title = "I don't like blogs"
36
37 # Default author
38 author = "Hartmut Kegan"
39
40 # Article encoding
41 encoding = "utf8"
42
43 #
44 # End of configuration
45 # DO *NOT* EDIT ANYTHING PAST HERE
46 #
47
48
49 import sys
50 import os
51 import errno
52 import shutil
53 import time
54 import datetime
55 import calendar
56 import zlib
57 import urllib
58 import cgi
59 from docutils.core import publish_parts
60 from docutils.utils import SystemMessage
61
62 # Before importing the config, add our cwd to the Python path
63 sys.path.append(os.getcwd())
64
65 # Load the config file, if there is one
66 try:
67         from config import *
68 except:
69         pass
70
71
72 # Pimp *_path config variables to support relative paths
73 data_path = os.path.realpath(data_path)
74 templates_path = os.path.realpath(templates_path)
75
76 # Default template
77
78 default_main_header = """\
79 <?xml version="1.0" encoding="utf-8"?>
80 <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
81           "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
82
83 <html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
84 <head>
85 <link rel="alternate" title="%(title)s" href="%(fullurl)s/atom"
86         type="application/atom+xml" />
87 <link href="%(css_url)s" rel="stylesheet" type="text/css" />
88 <title>%(title)s</title>
89 </head>
90
91 <body>
92
93 <h1><a href="%(url)s">%(title)s</a></h1>
94
95 <div class="content">
96 """
97
98 default_main_footer = """
99 </div>
100 <div class="footer">
101   %(showyear)s: %(monthlinks)s<br/>
102   years: %(yearlinks)s<br/>
103   subscribe: <a href="%(url)s/atom">atom</a><br/>
104   views: <a href="%(url)s/">blog</a> <a href="%(url)s/list">list</a><br/>
105 </div>
106
107 </body>
108 </html>
109 """
110
111 default_article_header = """
112 <div class="article">
113 <h2><a href="%(url)s/post/%(uuid)s">%(arttitle)s</a></h2>
114 <span class="artinfo">
115   by %(author)s on <span class="date">
116
117 <a class="date" href="%(url)s/%(cyear)d/">%(cyear)04d</a>-\
118 <a class="date" href="%(url)s/%(cyear)d/%(cmonth)d/">%(cmonth)02d</a>-\
119 <a class="date" href="%(url)s/%(cyear)d/%(cmonth)d/%(cday)d/">%(cday)02d</a>\
120     %(chour)02d:%(cminute)02d</span>
121   (updated on <span class="date">
122 <a class="date" href="%(url)s/%(uyear)d/">%(uyear)04d</a>-\
123 <a class="date" href="%(url)s/%(uyear)d/%(umonth)d/">%(umonth)02d</a>-\
124 <a class="date" href="%(url)s/%(uyear)d/%(umonth)d/%(uday)d/">%(uday)02d</a>\
125     %(uhour)02d:%(uminute)02d)</span><br/>
126   <span class="tags">tagged %(tags)s</span> -
127   <span class="comments">with %(comments)s
128     <a href="%(url)s/post/%(uuid)s#comments">comment(s)</a></span>
129 </span><br/>
130 <p/>
131 <div class="artbody">
132 """
133
134 default_article_footer = """
135 <p/>
136 </div>
137 </div>
138 """
139
140 default_comment_header = """
141 <div class="comment">
142 <a name="comment-%(number)d" />
143 <h3><a href="#comment-%(number)d">Comment #%(number)d</a></h3>
144 <span class="cominfo">by %(linked_author)s
145   on %(year)04d-%(month)02d-%(day)02d %(hour)02d:%(minute)02d</span>
146 <p/>
147 <div class="combody">
148 """
149
150 default_comment_footer = """
151 <p/>
152 </div>
153 </div>
154 """
155
156 default_comment_form = """
157 <div class="comform">
158 <a name="comment" />
159 <h3 class="comform"><a href="#comment">Your comment</a></h3>
160 <div class="comforminner">
161 <form method="%(form_method)s" action="%(form_action)s">
162 <div class="comformauthor">
163   <label for="comformauthor">Your name %(form_author_error)s</label>
164   <input type="text" class="comformauthor" id="comformauthor"
165          name="comformauthor" value="%(form_author)s" />
166 </div>
167 <div class="comformlink">
168   <label for="comformlink">Your link
169     <span class="comformoptional">(optional, will be published)</span>
170       %(form_link_error)s</label>
171   <input type="text" class="comformlink" id="comformlink"
172          name="comformlink" value="%(form_link)s" />
173   <div class="comformhelp">
174     like <span class="formurlexample">http://www.example.com/</span>
175     or <span class="formurlexample">mailto:you@example.com</span>
176   </div>
177 </div>
178 <div class="comformbody">
179   <label for="comformbody" class="comformbody">The comment
180     %(form_body_error)s</label>
181   <textarea class="comformbody" id="comformbody" name="comformbody" rows="15"
182             cols="80">%(form_body)s</textarea>
183   <div class="comformhelp">
184     in
185     <a href="http://docutils.sourceforge.net/docs/user/rst/quickref.html">\
186 RestructuredText</a> format, please
187   </div>
188 </div>
189 <div class="comformsend">
190   <button type="submit" class="comformsend" id="comformsend" name="comformsend">
191     Send comment
192   </button>
193 </div>
194 </form>
195 </div>
196 </div>
197 """
198
199 default_comment_error = '<span class="comformerror">(%(error)s)</span>'
200
201
202 # Default CSS
203 default_css = """
204 body {
205         font-family: sans-serif;
206         font-size: small;
207         width: 52em;
208 }
209
210 div.content {
211         width: 96%;
212 }
213
214 h1 {
215         font-size: large;
216         border-bottom: 2px solid #99F;
217         width: 100%;
218         margin-bottom: 1em;
219 }
220
221 h2 {
222         font-size: medium;
223         font-weigth: none;
224         margin-bottom: 1pt;
225         border-bottom: 1px solid #99C;
226 }
227
228 h3 {
229         font-size: small;
230         font-weigth: none;
231         margin-bottom: 1pt;
232         border-bottom: 1px solid #99C;
233 }
234
235 h1 a, h2 a, h3 a {
236         text-decoration: none;
237         color: black;
238 }
239
240 span.artinfo {
241         font-size: xx-small;
242 }
243
244 span.artinfo a {
245         text-decoration: none;
246         color: #339;
247 }
248
249 span.artinfo a:hover {
250         text-decoration: none;
251         color: blue;
252 }
253
254 div.artbody {
255         margin-left: 1em;
256 }
257
258 div.article {
259         margin-bottom: 2em;
260 }
261
262 span.cominfo {
263         font-size: xx-small;
264 }
265
266 span.cominfo a {
267         text-decoration: none;
268         color: #339;
269 }
270
271 span.cominfo a:hover {
272         text-decoration: none;
273         color: blue;
274 }
275
276 div.combody {
277         margin-left: 2em;
278 }
279
280 div.comment {
281         margin-left: 1em;
282         margin-bottom: 1em;
283 }
284
285 div.comforminner {
286         margin-left: 2em;
287 }
288
289 div.comform {
290         margin-left: 1em;
291         margin-bottom: 1em;
292 }
293
294 div.comform label {
295         display: block;
296         border-bottom: 1px solid #99C;
297         margin-top: 0.5em;
298         clear: both;
299 }
300
301 div.comform span.comformoptional {
302         font-size: xx-small;
303         color: #666;
304 }
305
306 div.comform input {
307         font-size: small;
308         width: 99%;
309 }
310
311 div.comformhelp {
312         font-size: xx-small;
313         text-align: right;
314         float: right;
315 }
316
317 span.formurlexample {
318         color: #111;
319         background-color: #EEF;
320         font-family: monospace;
321         padding-left: 0.2em;
322         padding-right: 0.2em;
323 }
324
325 textarea.comformbody {
326         font-family: monospace;
327         font-size: small;
328         width: 99%;
329         height: 15em;
330 }
331
332 button.comformsend {
333         margin-top: 0.5em;
334 }
335
336 span.comformerror {
337         color: #900;
338         font-size: xx-small;
339         margin-left: 0.5em;
340 }
341
342 hr {
343         float: left;
344         height: 2px;
345         border: 0;
346         background-color: #99F;
347         width: 60%;
348 }
349
350 div.footer {
351         margin-top: 1em;
352         padding-top: 0.4em;
353         width: 100%;
354         border-top: 2px solid #99F;
355         font-size: x-small;
356 }
357
358 div.footer a {
359         text-decoration: none;
360 }
361
362 /* Articles are enclosed in <div class="section"> */
363 div.section h1 {
364         font-size: small;
365         font-weigth: none;
366         width: 100%;
367         margin-bottom: 1pt;
368         border-bottom: 1px dotted #99C;
369 }
370
371 """
372
373 # helper functions
374 def rst_to_html(rst):
375         settings = {
376                 'input_encoding': encoding,
377                 'output_encoding': 'utf8',
378                 'halt_level': 1,
379                 'traceback':  1,
380         }
381         parts = publish_parts(rst, settings_overrides = settings,
382                                 writer_name = "html")
383         return parts['body'].encode('utf8')
384
385 def validate_rst(rst):
386         try:
387                 rst_to_html(rst)
388                 return None
389         except SystemMessage, e:
390                 desc = e.args[0].encode('utf-8') # the error string
391                 desc = desc[9:] # remove "<string>:"
392                 line = int(desc[:desc.find(':')]) # get just the line number
393                 desc = desc[desc.find(')')+2:-1] # remove (LEVEL/N)
394                 try:
395                         desc, context = desc.split('\n', 1)
396                 except ValueError:
397                         context = ''
398                 if desc.endswith('.'):
399                         desc = desc[:-1]
400                 return (line, desc, context)
401
402 def sanitize(obj):
403         if isinstance(obj, basestring):
404                 return cgi.escape(obj, True)
405         return obj
406
407
408 # find out our URL, needed for syndication
409 try:
410         n = os.environ['SERVER_NAME']
411         p = os.environ['SERVER_PORT']
412         s = os.environ['SCRIPT_NAME']
413         if p == '80': p = ''
414         else: p = ':' + p
415         full_url = 'http://%s%s%s' % (n, p, s)
416 except KeyError:
417         full_url = 'Not needed'
418
419
420 class Templates (object):
421         def __init__(self, tpath, db, showyear = None):
422                 self.tpath = tpath
423                 self.db = db
424                 now = datetime.datetime.now()
425                 if not showyear:
426                         showyear = now.year
427
428                 self.vars = {
429                         'css_url': css_url,
430                         'title': title,
431                         'url': blog_url,
432                         'fullurl': full_url,
433                         'year': now.year,
434                         'month': now.month,
435                         'day': now.day,
436                         'showyear': showyear,
437                         'monthlinks': ' '.join(db.get_month_links(showyear)),
438                         'yearlinks': ' '.join(db.get_year_links()),
439                 }
440
441         def get_template(self, page_name, default_template, extra_vars = None):
442                 if extra_vars is None:
443                         vars = self.vars
444                 else:
445                         vars = self.vars.copy()
446                         vars.update(extra_vars)
447
448                 p = '%s/%s.html' % (self.tpath, page_name)
449                 if os.path.isfile(p):
450                         return open(p).read() % vars
451                 return default_template % vars
452
453         def get_main_header(self):
454                 return self.get_template('header', default_main_header)
455
456         def get_main_footer(self):
457                 return self.get_template('footer', default_main_footer)
458
459         def get_article_header(self, article):
460                 return self.get_template(
461                         'art_header', default_article_header, article.to_vars())
462
463         def get_article_footer(self, article):
464                 return self.get_template(
465                         'art_footer', default_article_footer, article.to_vars())
466
467         def get_comment_header(self, comment):
468                 vars = comment.to_vars()
469                 if comment.link:
470                         vars['linked_author'] = '<a href="%s">%s</a>' \
471                                         % (comment.link, comment.author)
472                 else:
473                         vars['linked_author'] = comment.author
474                 return self.get_template(
475                         'com_header', default_comment_header, vars)
476
477         def get_comment_footer(self, comment):
478                 return self.get_template(
479                         'com_footer', default_comment_footer, comment.to_vars())
480
481         def get_comment_form(self, article, form_data):
482                 vars = article.to_vars()
483                 vars.update(form_data.to_vars(self))
484                 return self.get_template(
485                         'com_form', default_comment_form, vars)
486
487         def get_comment_error(self, error):
488                 return self.get_template(
489                         'com_error', default_comment_error, dict(error=error))
490
491
492 class CommentFormData (object):
493         def __init__(self, author = '', link = '', body = ''):
494                 self.author = author
495                 self.link = link
496                 self.body = body
497                 self.author_error = ''
498                 self.link_error = ''
499                 self.body_error = ''
500                 self.action = ''
501                 self.method = 'post'
502
503         def to_vars(self, template):
504                 render_error = template.get_comment_error
505                 a_error = self.author_error and render_error(self.author_error)
506                 l_error = self.link_error and render_error(self.link_error)
507                 b_error = self.body_error and render_error(self.body_error)
508                 return {
509                         'form_author': sanitize(self.author),
510                         'form_link': sanitize(self.link),
511                         'form_body': sanitize(self.body),
512
513                         'form_author_error': a_error,
514                         'form_link_error': l_error,
515                         'form_body_error': b_error,
516
517                         'form_action': self.action,
518                         'form_method': self.method,
519                 }
520
521
522 class Comment (object):
523         def __init__(self, article, number, created = None):
524                 self.article = article
525                 self.number = number
526                 if created is None:
527                         self.created = datetime.datetime.now()
528                 else:
529                         self.created = created
530
531                 self.loaded = False
532
533                 # loaded on demand
534                 self._author = author
535                 self._link = ''
536                 self._raw_content = 'Removed comment'
537
538
539         def get_author(self):
540                 if not self.loaded:
541                         self.load()
542                 return self._author
543         author = property(fget = get_author)
544
545         def get_link(self):
546                 if not self.loaded:
547                         self.load()
548                 return self._link
549         link = property(fget = get_link)
550
551         def get_raw_content(self):
552                 if not self.loaded:
553                         self.load()
554                 return self._raw_content
555         raw_content = property(fget = get_raw_content)
556
557
558         def set(self, author, raw_content, link = '', created = None):
559                 self.loaded = True
560                 self._author = author
561                 self._raw_content = raw_content
562                 self._link = link
563                 self.created = created or datetime.datetime.now()
564
565
566         def load(self):
567                 filename = os.path.join(comments_path, self.article.uuid,
568                                         str(self.number))
569                 try:
570                         raw = open(filename).readlines()
571                 except:
572                         return
573
574                 count = 0
575                 for l in raw:
576                         if ':' in l:
577                                 name, value = l.split(':', 1)
578                                 if name.lower() == 'author':
579                                         self._author = value.strip()
580                                 elif name.lower() == 'link':
581                                         self._link = value.strip()
582                         elif l == '\n':
583                                 # end of header
584                                 break
585                         count += 1
586                 self._raw_content = ''.join(raw[count + 1:])
587                 self.loaded = True
588
589         def save(self):
590                 filename = os.path.join(comments_path, self.article.uuid,
591                                         str(self.number))
592                 try:
593                         f = open(filename, 'w')
594                         f.write('Author: %s\n' % self.author)
595                         f.write('Link: %s\n' % self.link)
596                         f.write('\n')
597                         f.write(self.raw_content)
598                 except:
599                         return
600
601
602         def to_html(self):
603                 return rst_to_html(self.raw_content)
604
605         def to_vars(self):
606                 return {
607                         'number': self.number,
608                         'author': sanitize(self.author),
609                         'link': sanitize(self.link),
610                         'date': self.created.isoformat(' '),
611                         'created': self.created.isoformat(' '),
612
613                         'year': self.created.year,
614                         'month': self.created.month,
615                         'day': self.created.day,
616                         'hour': self.created.hour,
617                         'minute': self.created.minute,
618                         'second': self.created.second,
619                 }
620
621 class CommentDB (object):
622         def __init__(self, article):
623                 self.path = os.path.join(comments_path, article.uuid)
624                 self.comments = []
625                 self.load(article)
626
627         def load(self, article):
628                 try:
629                         f = open(os.path.join(self.path, 'db'))
630                 except:
631                         return
632
633                 for l in f:
634                         # Each line has the following comma separated format:
635                         # number, created (epoch)
636                         # Empty lines are meaningful and represent removed
637                         # comments (so we can preserve the comment number)
638                         l = l.split(',')
639                         try:
640                                 n = int(l[0])
641                                 d = datetime.datetime.fromtimestamp(float(l[1]))
642                         except:
643                                 # Removed/invalid comment
644                                 self.comments.append(None)
645                                 continue
646                         self.comments.append(Comment(article, n, d))
647
648         def save(self):
649                 old_db = os.path.join(self.path, 'db')
650                 new_db = os.path.join(self.path, 'db.tmp')
651                 f = open(new_db, 'w')
652                 for c in self.comments:
653                         s = ''
654                         if c is not None:
655                                 s = ''
656                                 s += str(c.number) + ', '
657                                 s += str(time.mktime(c.created.timetuple()))
658                         s += '\n'
659                         f.write(s)
660                 f.close()
661                 os.rename(new_db, old_db)
662
663
664 class Article (object):
665         def __init__(self, path, created = None, updated = None):
666                 self.path = path
667                 self.created = created
668                 self.updated = updated
669                 self.uuid = "%08x" % zlib.crc32(self.path)
670
671                 self.loaded = False
672
673                 # loaded on demand
674                 self._title = 'Removed post'
675                 self._author = author
676                 self._tags = []
677                 self._raw_content = ''
678                 self._comments = []
679
680
681         def get_title(self):
682                 if not self.loaded:
683                         self.load()
684                 return self._title
685         title = property(fget = get_title)
686
687         def get_author(self):
688                 if not self.loaded:
689                         self.load()
690                 return self._author
691         author = property(fget = get_author)
692
693         def get_tags(self):
694                 if not self.loaded:
695                         self.load()
696                 return self._tags
697         tags = property(fget = get_tags)
698
699         def get_raw_content(self):
700                 if not self.loaded:
701                         self.load()
702                 return self._raw_content
703         raw_content = property(fget = get_raw_content)
704
705         def get_comments(self):
706                 if not self.loaded:
707                         self.load()
708                 return self._comments
709         comments = property(fget = get_comments)
710
711
712         def __cmp__(self, other):
713                 if self.path == other.path:
714                         return 0
715                 if not self.created:
716                         return 1
717                 if not other.created:
718                         return -1
719                 if self.created < other.created:
720                         return -1
721                 return 1
722
723         def title_cmp(self, other):
724                 return cmp(self.title, other.title)
725
726
727         def add_comment(self, author, raw_content, link = ''):
728                 c = Comment(self, len(self.comments))
729                 c.set(author, raw_content, link)
730                 self.comments.append(c)
731                 return c
732
733
734         def load(self):
735                 # XXX this tweak is only needed for old DB format, where
736                 # article's paths started with a slash
737                 path = self.path
738                 if path.startswith('/'):
739                         path = path[1:]
740                 filename = os.path.join(data_path, path)
741                 try:
742                         raw = open(filename).readlines()
743                 except:
744                         return
745
746                 count = 0
747                 for l in raw:
748                         if ':' in l:
749                                 name, value = l.split(':', 1)
750                                 if name.lower() == 'title':
751                                         self._title = value.strip()
752                                 elif name.lower() == 'author':
753                                         self._author = value.strip()
754                                 elif name.lower() == 'tags':
755                                         ts = value.split(',')
756                                         ts = [t.strip() for t in ts]
757                                         self._tags = set(ts)
758                         elif l == '\n':
759                                 # end of header
760                                 break
761                         count += 1
762                 self._raw_content = ''.join(raw[count + 1:])
763                 db = CommentDB(self)
764                 self._comments = db.comments
765                 self.loaded = True
766
767         def to_html(self):
768                 return rst_to_html(self.raw_content)
769
770         def to_vars(self):
771                 return {
772                         'arttitle': sanitize(self.title),
773                         'author': sanitize(self.author),
774                         'date': self.created.isoformat(' '),
775                         'uuid': self.uuid,
776                         'tags': self.get_tags_links(),
777                         'comments': len(self.comments),
778
779                         'created': self.created.isoformat(' '),
780                         'ciso': self.created.isoformat(),
781                         'cyear': self.created.year,
782                         'cmonth': self.created.month,
783                         'cday': self.created.day,
784                         'chour': self.created.hour,
785                         'cminute': self.created.minute,
786                         'csecond': self.created.second,
787
788                         'updated': self.updated.isoformat(' '),
789                         'uiso': self.updated.isoformat(),
790                         'uyear': self.updated.year,
791                         'umonth': self.updated.month,
792                         'uday': self.updated.day,
793                         'uhour': self.updated.hour,
794                         'uminute': self.updated.minute,
795                         'usecond': self.updated.second,
796                 }
797
798         def get_tags_links(self):
799                 l = []
800                 tags = list(self.tags)
801                 tags.sort()
802                 for t in tags:
803                         l.append('<a class="tag" href="%s/tag/%s">%s</a>' % \
804                                 (blog_url, urllib.quote(t), sanitize(t) ))
805                 return ', '.join(l)
806
807
808 class ArticleDB (object):
809         def __init__(self, dbpath):
810                 self.dbpath = dbpath
811                 self.articles = []
812                 self.uuids = {}
813                 self.actyears = set()
814                 self.actmonths = set()
815                 self.load()
816
817         def get_articles(self, year = 0, month = 0, day = 0, tags = None):
818                 l = []
819                 for a in self.articles:
820                         if year and a.created.year != year: continue
821                         if month and a.created.month != month: continue
822                         if day and a.created.day != day: continue
823                         if tags and not tags.issubset(a.tags): continue
824
825                         l.append(a)
826
827                 return l
828
829         def get_article(self, uuid):
830                 return self.uuids[uuid]
831
832         def load(self):
833                 try:
834                         f = open(self.dbpath)
835                 except:
836                         return
837
838                 for l in f:
839                         # Each line has the following comma separated format:
840                         # path (relative to data_path), \
841                         #       created (epoch), \
842                         #       updated (epoch)
843                         try:
844                                 l = l.split(',')
845                         except:
846                                 continue
847
848                         a = Article(l[0],
849                                 datetime.datetime.fromtimestamp(float(l[1])),
850                                 datetime.datetime.fromtimestamp(float(l[2])))
851                         self.uuids[a.uuid] = a
852                         self.actyears.add(a.created.year)
853                         self.actmonths.add((a.created.year, a.created.month))
854                         self.articles.append(a)
855
856         def save(self):
857                 f = open(self.dbpath + '.tmp', 'w')
858                 for a in self.articles:
859                         s = ''
860                         s += a.path + ', '
861                         s += str(time.mktime(a.created.timetuple())) + ', '
862                         s += str(time.mktime(a.updated.timetuple())) + '\n'
863                         f.write(s)
864                 f.close()
865                 os.rename(self.dbpath + '.tmp', self.dbpath)
866
867         def get_year_links(self):
868                 yl = list(self.actyears)
869                 yl.sort(reverse = True)
870                 return [ '<a href="%s/%d/">%d</a>' % (blog_url, y, y)
871                                 for y in yl ]
872
873         def get_month_links(self, year):
874                 am = [ i[1] for i in self.actmonths if i[0] == year ]
875                 ml = []
876                 for i in range(1, 13):
877                         name = calendar.month_name[i][:3]
878                         if i in am:
879                                 s = '<a href="%s/%d/%d/">%s</a>' % \
880                                         ( blog_url, year, i, name )
881                         else:
882                                 s = name
883                         ml.append(s)
884                 return ml
885
886 #
887 # Main
888 #
889
890 def render_comments(article, template, form_data):
891         print '<a name="comments" />'
892         for c in article.comments:
893                 if c is None:
894                         continue
895                 print template.get_comment_header(c)
896                 print c.to_html()
897                 print template.get_comment_footer(c)
898         if not form_data:
899                 form_data = CommentFormData()
900         form_data.action = blog_url + '/comment/' + article.uuid + '#comment'
901         print template.get_comment_form(article, form_data)             ,
902
903 def render_html(articles, db, actyear = None, show_comments = False,
904                 redirect =  None, form_data = None):
905         if redirect:
906                 print 'Status: 303 See Other\r\n',
907                 print 'Location: %s\r\n' % redirect,
908         print 'Content-type: text/html; charset=utf-8\r\n',
909         print '\r\n',
910         template = Templates(templates_path, db, actyear)
911         print template.get_main_header()
912         for a in articles:
913                 print template.get_article_header(a)
914                 print a.to_html()
915                 print template.get_article_footer(a)
916                 if show_comments:
917                         render_comments(a, template, form_data)
918         print template.get_main_footer()
919
920 def render_artlist(articles, db, actyear = None):
921         template = Templates(templates_path, db, actyear)
922         print 'Content-type: text/html; charset=utf-8\n'
923         print template.get_main_header()
924         print '<h2>Articles</h2>'
925         for a in articles:
926                 print '<li><a href="%(url)s/uuid/%(uuid)s">%(title)s</a></li>' \
927                         % {     'url': blog_url,
928                                 'uuid': a.uuid,
929                                 'title': a.title,
930                                 'author': a.author,
931                         }
932         print template.get_main_footer()
933
934 def render_atom(articles):
935         if len(articles) > 0:
936                 updated = articles[0].updated.isoformat()
937         else:
938                 updated = datetime.datetime.now().isoformat()
939
940         print 'Content-type: application/atom+xml; charset=utf-8\n'
941         print """<?xml version="1.0" encoding="utf-8"?>
942
943 <feed xmlns="http://www.w3.org/2005/Atom">
944  <title>%(title)s</title>
945  <link rel="alternate" type="text/html" href="%(url)s"/>
946  <link rel="self" type="application/atom+xml" href="%(url)s/atom"/>
947  <id>%(url)s</id> <!-- TODO: find a better <id>, see RFC 4151 -->
948  <updated>%(updated)sZ</updated>
949
950         """ % {
951                 'title': title,
952                 'url': full_url,
953                 'updated': updated,
954         }
955
956         for a in articles:
957                 vars = a.to_vars()
958                 vars.update( {
959                         'url': full_url,
960                         'contents': a.to_html(),
961                 } )
962                 print """
963   <entry>
964     <title>%(arttitle)s</title>
965     <author><name>%(author)s</name></author>
966     <link href="%(url)s/post/%(uuid)s" />
967     <id>%(url)s/post/%(uuid)s</id>
968     <summary>%(arttitle)s</summary>
969     <published>%(ciso)sZ</published>
970     <updated>%(uiso)sZ</updated>
971     <content type="xhtml">
972       <div xmlns="http://www.w3.org/1999/xhtml"><p>
973 %(contents)s
974       </p></div>
975     </content>
976   </entry>
977                 """ % vars
978         print "</feed>"
979
980
981 def render_style():
982         print 'Content-type: text/css\r\n\r\n',
983         print default_css
984
985 def handle_cgi():
986         import cgitb; cgitb.enable()
987
988         form = cgi.FieldStorage()
989         year = int(form.getfirst("year", 0))
990         month = int(form.getfirst("month", 0))
991         day = int(form.getfirst("day", 0))
992         tags = set(form.getlist("tag"))
993         uuid = None
994         atom = False
995         style = False
996         post = False
997         artlist = False
998         comment = False
999
1000         if os.environ.has_key('PATH_INFO'):
1001                 path_info = os.environ['PATH_INFO']
1002                 style = path_info == '/style'
1003                 atom = path_info == '/atom'
1004                 tag = path_info.startswith('/tag/')
1005                 post = path_info.startswith('/post/')
1006                 artlist = path_info.startswith('/list')
1007                 comment = path_info.startswith('/comment/') and enable_comments
1008                 if not style and not atom and not post and not tag \
1009                                 and not comment and not artlist:
1010                         date = path_info.split('/')[1:]
1011                         try:
1012                                 if len(date) > 1 and date[0]:
1013                                         year = int(date[0])
1014                                 if len(date) > 2 and date[1]:
1015                                         month = int(date[1])
1016                                 if len(date) > 3 and date[2]:
1017                                         day = int(date[2])
1018                         except ValueError:
1019                                 pass
1020                 elif post:
1021                         uuid = path_info.replace('/post/', '')
1022                         uuid = uuid.replace('/', '')
1023                 elif tag:
1024                         t = path_info.replace('/tag/', '')
1025                         t = t.replace('/', '')
1026                         t = urllib.unquote_plus(t)
1027                         tags = set((t,))
1028                 elif comment:
1029                         uuid = path_info.replace('/comment/', '')
1030                         uuid = uuid.replace('#comment', '')
1031                         uuid = uuid.replace('/', '')
1032                         author = form.getfirst('comformauthor', '')
1033                         link = form.getfirst('comformlink', '')
1034                         body = form.getfirst('comformbody', '')
1035
1036         db = ArticleDB(os.path.join(data_path, 'db'))
1037         if atom:
1038                 articles = db.get_articles(tags = tags)
1039                 articles.sort(reverse = True)
1040                 render_atom(articles[:10])
1041         elif style:
1042                 render_style()
1043         elif post:
1044                 render_html( [db.get_article(uuid)], db, year, enable_comments )
1045         elif artlist:
1046                 articles = db.get_articles()
1047                 articles.sort(cmp = Article.title_cmp)
1048                 render_artlist(articles, db)
1049         elif comment:
1050                 form_data = CommentFormData(author.strip().replace('\n', ' '),
1051                                 link.strip().replace('\n', ' '), body.strip())
1052                 article = db.get_article(uuid)
1053                 redirect = False
1054                 valid = True
1055                 if not form_data.author:
1056                         form_data.author_error = 'please, enter your name'
1057                         valid = False
1058                 if not form_data.body:
1059                         form_data.body_error = 'please, write a comment'
1060                         valid = False
1061                 else:
1062                         error = validate_rst(form_data.body)
1063                         if error is not None:
1064                                 (line, desc, ctx) = error
1065                                 form_data.body_error = 'error at line %d: %s' \
1066                                                 % (line, desc)
1067                                 valid = False
1068                 if valid:
1069                         c = article.add_comment(author, body, link)
1070                         c.save()
1071                         cdb = CommentDB(article)
1072                         cdb.comments = article.comments
1073                         cdb.save()
1074                         redirect = blog_url + '/post/' + uuid + '#comment-' \
1075                                         + str(c.number)
1076                 render_html( [article], db, year, enable_comments, redirect,
1077                                 form_data )
1078         else:
1079                 articles = db.get_articles(year, month, day, tags)
1080                 articles.sort(reverse = True)
1081                 if not year and not month and not day and not tags:
1082                         articles = articles[:10]
1083                 render_html(articles, db, year)
1084
1085
1086 def usage():
1087         print 'Usage: %s {add|rm|update} article_path' % sys.argv[0]
1088
1089 def handle_cmd():
1090         if len(sys.argv) != 3:
1091                 usage()
1092                 return 1
1093
1094         cmd = sys.argv[1]
1095         art_path = os.path.realpath(sys.argv[2])
1096
1097         if os.path.commonprefix([data_path, art_path]) != data_path:
1098                 print "Error: article (%s) must be inside data_path (%s)" % \
1099                                 (art_path, data_path)
1100                 return 1
1101         art_path = art_path[len(data_path)+1:]
1102
1103         db_filename = os.path.join(data_path, 'db')
1104         if not os.path.isfile(db_filename):
1105                 open(db_filename, 'w').write('')
1106         db = ArticleDB(db_filename)
1107
1108         if cmd == 'add':
1109                 article = Article(art_path, datetime.datetime.now(),
1110                                         datetime.datetime.now())
1111                 for a in db.articles:
1112                         if a == article:
1113                                 print 'Error: article already exists'
1114                                 return 1
1115                 db.articles.append(article)
1116                 db.save()
1117                 if enable_comments:
1118                         comment_dir = os.path.join(comments_path, article.uuid)
1119                         try:
1120                                 os.mkdir(comment_dir, 0775)
1121                         except OSError, e:
1122                                 if e.errno != errno.EEXIST:
1123                                         print "Error: can't create comments " \
1124                                                 "directory %s (%s)" \
1125                                                         % (comment_dir, e)
1126                                 # otherwise is probably a removed and re-added
1127                                 # article
1128         elif cmd == 'rm':
1129                 article = Article(art_path)
1130                 for a in db.articles:
1131                         if a == article:
1132                                 break
1133                 else:
1134                         print "Error: no such article"
1135                         return 1
1136                 if enable_comments:
1137                         r = raw_input('Remove comments [y/N]? ')
1138                 db.articles.remove(a)
1139                 db.save()
1140                 if enable_comments and r.lower() == 'y':
1141                         shutil.rmtree(os.path.join(comments_path, a.uuid))
1142         elif cmd == 'update':
1143                 article = Article(art_path)
1144                 for a in db.articles:
1145                         if a == article:
1146                                 break
1147                 else:
1148                         print "Error: no such article"
1149                         return 1
1150                 a.updated = datetime.datetime.now()
1151                 db.save()
1152         else:
1153                 usage()
1154                 return 1
1155
1156         return 0
1157
1158
1159 if os.environ.has_key('GATEWAY_INTERFACE'):
1160         handle_cgi()
1161 else:
1162         sys.exit(handle_cmd())
1163
1164