]> git.llucax.com Git - software/blitiri.git/blob - blitiri.cgi
Validate link form field when adding a comment online
[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 valid_link(link):
403         import re
404         mail_re = r"^[^ \t\n\r@<>()]+@[a-z0-9][a-z0-9\.\-_]*\.[a-z]+$"
405         scheme_re = r'^[a-zA-Z]+:'
406         url_re = r'^(?:[a-z0-9\-]+|[a-z0-9][a-z0-9\-\.\_]*\.[a-z]+)' \
407                         r'(?::[0-9]+)?(?:/.*)?$'
408         scheme = ''
409         rest = link
410         if re.match(scheme_re, link, re.I):
411                 scheme, rest = link.split(':', 1)
412         if (not scheme or scheme == 'mailto') and re.match(mail_re, rest, re.I):
413                 return 'mailto:' + link
414         if not scheme and re.match(url_re, rest, re.I):
415                 return 'http://' + rest
416         if scheme:
417                 return link
418         return None
419
420 def sanitize(obj):
421         if isinstance(obj, basestring):
422                 return cgi.escape(obj, True)
423         return obj
424
425
426 # find out our URL, needed for syndication
427 try:
428         n = os.environ['SERVER_NAME']
429         p = os.environ['SERVER_PORT']
430         s = os.environ['SCRIPT_NAME']
431         if p == '80': p = ''
432         else: p = ':' + p
433         full_url = 'http://%s%s%s' % (n, p, s)
434 except KeyError:
435         full_url = 'Not needed'
436
437
438 class Templates (object):
439         def __init__(self, tpath, db, showyear = None):
440                 self.tpath = tpath
441                 self.db = db
442                 now = datetime.datetime.now()
443                 if not showyear:
444                         showyear = now.year
445
446                 self.vars = {
447                         'css_url': css_url,
448                         'title': title,
449                         'url': blog_url,
450                         'fullurl': full_url,
451                         'year': now.year,
452                         'month': now.month,
453                         'day': now.day,
454                         'showyear': showyear,
455                         'monthlinks': ' '.join(db.get_month_links(showyear)),
456                         'yearlinks': ' '.join(db.get_year_links()),
457                 }
458
459         def get_template(self, page_name, default_template, extra_vars = None):
460                 if extra_vars is None:
461                         vars = self.vars
462                 else:
463                         vars = self.vars.copy()
464                         vars.update(extra_vars)
465
466                 p = '%s/%s.html' % (self.tpath, page_name)
467                 if os.path.isfile(p):
468                         return open(p).read() % vars
469                 return default_template % vars
470
471         def get_main_header(self):
472                 return self.get_template('header', default_main_header)
473
474         def get_main_footer(self):
475                 return self.get_template('footer', default_main_footer)
476
477         def get_article_header(self, article):
478                 return self.get_template(
479                         'art_header', default_article_header, article.to_vars())
480
481         def get_article_footer(self, article):
482                 return self.get_template(
483                         'art_footer', default_article_footer, article.to_vars())
484
485         def get_comment_header(self, comment):
486                 vars = comment.to_vars()
487                 if comment.link:
488                         vars['linked_author'] = '<a href="%s">%s</a>' \
489                                         % (comment.link, comment.author)
490                 else:
491                         vars['linked_author'] = comment.author
492                 return self.get_template(
493                         'com_header', default_comment_header, vars)
494
495         def get_comment_footer(self, comment):
496                 return self.get_template(
497                         'com_footer', default_comment_footer, comment.to_vars())
498
499         def get_comment_form(self, article, form_data):
500                 vars = article.to_vars()
501                 vars.update(form_data.to_vars(self))
502                 return self.get_template(
503                         'com_form', default_comment_form, vars)
504
505         def get_comment_error(self, error):
506                 return self.get_template(
507                         'com_error', default_comment_error, dict(error=error))
508
509
510 class CommentFormData (object):
511         def __init__(self, author = '', link = '', body = ''):
512                 self.author = author
513                 self.link = link
514                 self.body = body
515                 self.author_error = ''
516                 self.link_error = ''
517                 self.body_error = ''
518                 self.action = ''
519                 self.method = 'post'
520
521         def to_vars(self, template):
522                 render_error = template.get_comment_error
523                 a_error = self.author_error and render_error(self.author_error)
524                 l_error = self.link_error and render_error(self.link_error)
525                 b_error = self.body_error and render_error(self.body_error)
526                 return {
527                         'form_author': sanitize(self.author),
528                         'form_link': sanitize(self.link),
529                         'form_body': sanitize(self.body),
530
531                         'form_author_error': a_error,
532                         'form_link_error': l_error,
533                         'form_body_error': b_error,
534
535                         'form_action': self.action,
536                         'form_method': self.method,
537                 }
538
539
540 class Comment (object):
541         def __init__(self, article, number, created = None):
542                 self.article = article
543                 self.number = number
544                 if created is None:
545                         self.created = datetime.datetime.now()
546                 else:
547                         self.created = created
548
549                 self.loaded = False
550
551                 # loaded on demand
552                 self._author = author
553                 self._link = ''
554                 self._raw_content = 'Removed comment'
555
556
557         def get_author(self):
558                 if not self.loaded:
559                         self.load()
560                 return self._author
561         author = property(fget = get_author)
562
563         def get_link(self):
564                 if not self.loaded:
565                         self.load()
566                 return self._link
567         link = property(fget = get_link)
568
569         def get_raw_content(self):
570                 if not self.loaded:
571                         self.load()
572                 return self._raw_content
573         raw_content = property(fget = get_raw_content)
574
575
576         def set(self, author, raw_content, link = '', created = None):
577                 self.loaded = True
578                 self._author = author
579                 self._raw_content = raw_content
580                 self._link = link
581                 self.created = created or datetime.datetime.now()
582
583
584         def load(self):
585                 filename = os.path.join(comments_path, self.article.uuid,
586                                         str(self.number))
587                 try:
588                         raw = open(filename).readlines()
589                 except:
590                         return
591
592                 count = 0
593                 for l in raw:
594                         if ':' in l:
595                                 name, value = l.split(':', 1)
596                                 if name.lower() == 'author':
597                                         self._author = value.strip()
598                                 elif name.lower() == 'link':
599                                         self._link = value.strip()
600                         elif l == '\n':
601                                 # end of header
602                                 break
603                         count += 1
604                 self._raw_content = ''.join(raw[count + 1:])
605                 self.loaded = True
606
607         def save(self):
608                 filename = os.path.join(comments_path, self.article.uuid,
609                                         str(self.number))
610                 try:
611                         f = open(filename, 'w')
612                         f.write('Author: %s\n' % self.author)
613                         f.write('Link: %s\n' % self.link)
614                         f.write('\n')
615                         f.write(self.raw_content)
616                 except:
617                         return
618
619
620         def to_html(self):
621                 return rst_to_html(self.raw_content)
622
623         def to_vars(self):
624                 return {
625                         'number': self.number,
626                         'author': sanitize(self.author),
627                         'link': sanitize(self.link),
628                         'date': self.created.isoformat(' '),
629                         'created': self.created.isoformat(' '),
630
631                         'year': self.created.year,
632                         'month': self.created.month,
633                         'day': self.created.day,
634                         'hour': self.created.hour,
635                         'minute': self.created.minute,
636                         'second': self.created.second,
637                 }
638
639 class CommentDB (object):
640         def __init__(self, article):
641                 self.path = os.path.join(comments_path, article.uuid)
642                 self.comments = []
643                 self.load(article)
644
645         def load(self, article):
646                 try:
647                         f = open(os.path.join(self.path, 'db'))
648                 except:
649                         return
650
651                 for l in f:
652                         # Each line has the following comma separated format:
653                         # number, created (epoch)
654                         # Empty lines are meaningful and represent removed
655                         # comments (so we can preserve the comment number)
656                         l = l.split(',')
657                         try:
658                                 n = int(l[0])
659                                 d = datetime.datetime.fromtimestamp(float(l[1]))
660                         except:
661                                 # Removed/invalid comment
662                                 self.comments.append(None)
663                                 continue
664                         self.comments.append(Comment(article, n, d))
665
666         def save(self):
667                 old_db = os.path.join(self.path, 'db')
668                 new_db = os.path.join(self.path, 'db.tmp')
669                 f = open(new_db, 'w')
670                 for c in self.comments:
671                         s = ''
672                         if c is not None:
673                                 s = ''
674                                 s += str(c.number) + ', '
675                                 s += str(time.mktime(c.created.timetuple()))
676                         s += '\n'
677                         f.write(s)
678                 f.close()
679                 os.rename(new_db, old_db)
680
681
682 class Article (object):
683         def __init__(self, path, created = None, updated = None):
684                 self.path = path
685                 self.created = created
686                 self.updated = updated
687                 self.uuid = "%08x" % zlib.crc32(self.path)
688
689                 self.loaded = False
690
691                 # loaded on demand
692                 self._title = 'Removed post'
693                 self._author = author
694                 self._tags = []
695                 self._raw_content = ''
696                 self._comments = []
697
698
699         def get_title(self):
700                 if not self.loaded:
701                         self.load()
702                 return self._title
703         title = property(fget = get_title)
704
705         def get_author(self):
706                 if not self.loaded:
707                         self.load()
708                 return self._author
709         author = property(fget = get_author)
710
711         def get_tags(self):
712                 if not self.loaded:
713                         self.load()
714                 return self._tags
715         tags = property(fget = get_tags)
716
717         def get_raw_content(self):
718                 if not self.loaded:
719                         self.load()
720                 return self._raw_content
721         raw_content = property(fget = get_raw_content)
722
723         def get_comments(self):
724                 if not self.loaded:
725                         self.load()
726                 return self._comments
727         comments = property(fget = get_comments)
728
729
730         def __cmp__(self, other):
731                 if self.path == other.path:
732                         return 0
733                 if not self.created:
734                         return 1
735                 if not other.created:
736                         return -1
737                 if self.created < other.created:
738                         return -1
739                 return 1
740
741         def title_cmp(self, other):
742                 return cmp(self.title, other.title)
743
744
745         def add_comment(self, author, raw_content, link = ''):
746                 c = Comment(self, len(self.comments))
747                 c.set(author, raw_content, link)
748                 self.comments.append(c)
749                 return c
750
751
752         def load(self):
753                 # XXX this tweak is only needed for old DB format, where
754                 # article's paths started with a slash
755                 path = self.path
756                 if path.startswith('/'):
757                         path = path[1:]
758                 filename = os.path.join(data_path, path)
759                 try:
760                         raw = open(filename).readlines()
761                 except:
762                         return
763
764                 count = 0
765                 for l in raw:
766                         if ':' in l:
767                                 name, value = l.split(':', 1)
768                                 if name.lower() == 'title':
769                                         self._title = value.strip()
770                                 elif name.lower() == 'author':
771                                         self._author = value.strip()
772                                 elif name.lower() == 'tags':
773                                         ts = value.split(',')
774                                         ts = [t.strip() for t in ts]
775                                         self._tags = set(ts)
776                         elif l == '\n':
777                                 # end of header
778                                 break
779                         count += 1
780                 self._raw_content = ''.join(raw[count + 1:])
781                 db = CommentDB(self)
782                 self._comments = db.comments
783                 self.loaded = True
784
785         def to_html(self):
786                 return rst_to_html(self.raw_content)
787
788         def to_vars(self):
789                 return {
790                         'arttitle': sanitize(self.title),
791                         'author': sanitize(self.author),
792                         'date': self.created.isoformat(' '),
793                         'uuid': self.uuid,
794                         'tags': self.get_tags_links(),
795                         'comments': len(self.comments),
796
797                         'created': self.created.isoformat(' '),
798                         'ciso': self.created.isoformat(),
799                         'cyear': self.created.year,
800                         'cmonth': self.created.month,
801                         'cday': self.created.day,
802                         'chour': self.created.hour,
803                         'cminute': self.created.minute,
804                         'csecond': self.created.second,
805
806                         'updated': self.updated.isoformat(' '),
807                         'uiso': self.updated.isoformat(),
808                         'uyear': self.updated.year,
809                         'umonth': self.updated.month,
810                         'uday': self.updated.day,
811                         'uhour': self.updated.hour,
812                         'uminute': self.updated.minute,
813                         'usecond': self.updated.second,
814                 }
815
816         def get_tags_links(self):
817                 l = []
818                 tags = list(self.tags)
819                 tags.sort()
820                 for t in tags:
821                         l.append('<a class="tag" href="%s/tag/%s">%s</a>' % \
822                                 (blog_url, urllib.quote(t), sanitize(t) ))
823                 return ', '.join(l)
824
825
826 class ArticleDB (object):
827         def __init__(self, dbpath):
828                 self.dbpath = dbpath
829                 self.articles = []
830                 self.uuids = {}
831                 self.actyears = set()
832                 self.actmonths = set()
833                 self.load()
834
835         def get_articles(self, year = 0, month = 0, day = 0, tags = None):
836                 l = []
837                 for a in self.articles:
838                         if year and a.created.year != year: continue
839                         if month and a.created.month != month: continue
840                         if day and a.created.day != day: continue
841                         if tags and not tags.issubset(a.tags): continue
842
843                         l.append(a)
844
845                 return l
846
847         def get_article(self, uuid):
848                 return self.uuids[uuid]
849
850         def load(self):
851                 try:
852                         f = open(self.dbpath)
853                 except:
854                         return
855
856                 for l in f:
857                         # Each line has the following comma separated format:
858                         # path (relative to data_path), \
859                         #       created (epoch), \
860                         #       updated (epoch)
861                         try:
862                                 l = l.split(',')
863                         except:
864                                 continue
865
866                         a = Article(l[0],
867                                 datetime.datetime.fromtimestamp(float(l[1])),
868                                 datetime.datetime.fromtimestamp(float(l[2])))
869                         self.uuids[a.uuid] = a
870                         self.actyears.add(a.created.year)
871                         self.actmonths.add((a.created.year, a.created.month))
872                         self.articles.append(a)
873
874         def save(self):
875                 f = open(self.dbpath + '.tmp', 'w')
876                 for a in self.articles:
877                         s = ''
878                         s += a.path + ', '
879                         s += str(time.mktime(a.created.timetuple())) + ', '
880                         s += str(time.mktime(a.updated.timetuple())) + '\n'
881                         f.write(s)
882                 f.close()
883                 os.rename(self.dbpath + '.tmp', self.dbpath)
884
885         def get_year_links(self):
886                 yl = list(self.actyears)
887                 yl.sort(reverse = True)
888                 return [ '<a href="%s/%d/">%d</a>' % (blog_url, y, y)
889                                 for y in yl ]
890
891         def get_month_links(self, year):
892                 am = [ i[1] for i in self.actmonths if i[0] == year ]
893                 ml = []
894                 for i in range(1, 13):
895                         name = calendar.month_name[i][:3]
896                         if i in am:
897                                 s = '<a href="%s/%d/%d/">%s</a>' % \
898                                         ( blog_url, year, i, name )
899                         else:
900                                 s = name
901                         ml.append(s)
902                 return ml
903
904 #
905 # Main
906 #
907
908 def render_comments(article, template, form_data):
909         print '<a name="comments" />'
910         for c in article.comments:
911                 if c is None:
912                         continue
913                 print template.get_comment_header(c)
914                 print c.to_html()
915                 print template.get_comment_footer(c)
916         if not form_data:
917                 form_data = CommentFormData()
918         form_data.action = blog_url + '/comment/' + article.uuid + '#comment'
919         print template.get_comment_form(article, form_data)             ,
920
921 def render_html(articles, db, actyear = None, show_comments = False,
922                 redirect =  None, form_data = None):
923         if redirect:
924                 print 'Status: 303 See Other\r\n',
925                 print 'Location: %s\r\n' % redirect,
926         print 'Content-type: text/html; charset=utf-8\r\n',
927         print '\r\n',
928         template = Templates(templates_path, db, actyear)
929         print template.get_main_header()
930         for a in articles:
931                 print template.get_article_header(a)
932                 print a.to_html()
933                 print template.get_article_footer(a)
934                 if show_comments:
935                         render_comments(a, template, form_data)
936         print template.get_main_footer()
937
938 def render_artlist(articles, db, actyear = None):
939         template = Templates(templates_path, db, actyear)
940         print 'Content-type: text/html; charset=utf-8\n'
941         print template.get_main_header()
942         print '<h2>Articles</h2>'
943         for a in articles:
944                 print '<li><a href="%(url)s/uuid/%(uuid)s">%(title)s</a></li>' \
945                         % {     'url': blog_url,
946                                 'uuid': a.uuid,
947                                 'title': a.title,
948                                 'author': a.author,
949                         }
950         print template.get_main_footer()
951
952 def render_atom(articles):
953         if len(articles) > 0:
954                 updated = articles[0].updated.isoformat()
955         else:
956                 updated = datetime.datetime.now().isoformat()
957
958         print 'Content-type: application/atom+xml; charset=utf-8\n'
959         print """<?xml version="1.0" encoding="utf-8"?>
960
961 <feed xmlns="http://www.w3.org/2005/Atom">
962  <title>%(title)s</title>
963  <link rel="alternate" type="text/html" href="%(url)s"/>
964  <link rel="self" type="application/atom+xml" href="%(url)s/atom"/>
965  <id>%(url)s</id> <!-- TODO: find a better <id>, see RFC 4151 -->
966  <updated>%(updated)sZ</updated>
967
968         """ % {
969                 'title': title,
970                 'url': full_url,
971                 'updated': updated,
972         }
973
974         for a in articles:
975                 vars = a.to_vars()
976                 vars.update( {
977                         'url': full_url,
978                         'contents': a.to_html(),
979                 } )
980                 print """
981   <entry>
982     <title>%(arttitle)s</title>
983     <author><name>%(author)s</name></author>
984     <link href="%(url)s/post/%(uuid)s" />
985     <id>%(url)s/post/%(uuid)s</id>
986     <summary>%(arttitle)s</summary>
987     <published>%(ciso)sZ</published>
988     <updated>%(uiso)sZ</updated>
989     <content type="xhtml">
990       <div xmlns="http://www.w3.org/1999/xhtml"><p>
991 %(contents)s
992       </p></div>
993     </content>
994   </entry>
995                 """ % vars
996         print "</feed>"
997
998
999 def render_style():
1000         print 'Content-type: text/css\r\n\r\n',
1001         print default_css
1002
1003 def handle_cgi():
1004         import cgitb; cgitb.enable()
1005
1006         form = cgi.FieldStorage()
1007         year = int(form.getfirst("year", 0))
1008         month = int(form.getfirst("month", 0))
1009         day = int(form.getfirst("day", 0))
1010         tags = set(form.getlist("tag"))
1011         uuid = None
1012         atom = False
1013         style = False
1014         post = False
1015         artlist = False
1016         comment = False
1017
1018         if os.environ.has_key('PATH_INFO'):
1019                 path_info = os.environ['PATH_INFO']
1020                 style = path_info == '/style'
1021                 atom = path_info == '/atom'
1022                 tag = path_info.startswith('/tag/')
1023                 post = path_info.startswith('/post/')
1024                 artlist = path_info.startswith('/list')
1025                 comment = path_info.startswith('/comment/') and enable_comments
1026                 if not style and not atom and not post and not tag \
1027                                 and not comment and not artlist:
1028                         date = path_info.split('/')[1:]
1029                         try:
1030                                 if len(date) > 1 and date[0]:
1031                                         year = int(date[0])
1032                                 if len(date) > 2 and date[1]:
1033                                         month = int(date[1])
1034                                 if len(date) > 3 and date[2]:
1035                                         day = int(date[2])
1036                         except ValueError:
1037                                 pass
1038                 elif post:
1039                         uuid = path_info.replace('/post/', '')
1040                         uuid = uuid.replace('/', '')
1041                 elif tag:
1042                         t = path_info.replace('/tag/', '')
1043                         t = t.replace('/', '')
1044                         t = urllib.unquote_plus(t)
1045                         tags = set((t,))
1046                 elif comment:
1047                         uuid = path_info.replace('/comment/', '')
1048                         uuid = uuid.replace('#comment', '')
1049                         uuid = uuid.replace('/', '')
1050                         author = form.getfirst('comformauthor', '')
1051                         link = form.getfirst('comformlink', '')
1052                         body = form.getfirst('comformbody', '')
1053
1054         db = ArticleDB(os.path.join(data_path, 'db'))
1055         if atom:
1056                 articles = db.get_articles(tags = tags)
1057                 articles.sort(reverse = True)
1058                 render_atom(articles[:10])
1059         elif style:
1060                 render_style()
1061         elif post:
1062                 render_html( [db.get_article(uuid)], db, year, enable_comments )
1063         elif artlist:
1064                 articles = db.get_articles()
1065                 articles.sort(cmp = Article.title_cmp)
1066                 render_artlist(articles, db)
1067         elif comment:
1068                 form_data = CommentFormData(author.strip().replace('\n', ' '),
1069                                 link.strip().replace('\n', ' '), body.strip())
1070                 article = db.get_article(uuid)
1071                 redirect = False
1072                 valid = True
1073                 if not form_data.author:
1074                         form_data.author_error = 'please, enter your name'
1075                         valid = False
1076                 if form_data.link:
1077                         link = valid_link(form_data.link)
1078                         if link:
1079                                 form_data.link = link
1080                         else:
1081                                 form_data.link_error = 'please, enter a ' \
1082                                                 'valid link'
1083                                 valid = False
1084                 if not form_data.body:
1085                         form_data.body_error = 'please, write a comment'
1086                         valid = False
1087                 else:
1088                         error = validate_rst(form_data.body)
1089                         if error is not None:
1090                                 (line, desc, ctx) = error
1091                                 form_data.body_error = 'error at line %d: %s' \
1092                                                 % (line, desc)
1093                                 valid = False
1094                 if valid:
1095                         c = article.add_comment(author, body, link)
1096                         c.save()
1097                         cdb = CommentDB(article)
1098                         cdb.comments = article.comments
1099                         cdb.save()
1100                         redirect = blog_url + '/post/' + uuid + '#comment-' \
1101                                         + str(c.number)
1102                 render_html( [article], db, year, enable_comments, redirect,
1103                                 form_data )
1104         else:
1105                 articles = db.get_articles(year, month, day, tags)
1106                 articles.sort(reverse = True)
1107                 if not year and not month and not day and not tags:
1108                         articles = articles[:10]
1109                 render_html(articles, db, year)
1110
1111
1112 def usage():
1113         print 'Usage: %s {add|rm|update} article_path' % sys.argv[0]
1114
1115 def handle_cmd():
1116         if len(sys.argv) != 3:
1117                 usage()
1118                 return 1
1119
1120         cmd = sys.argv[1]
1121         art_path = os.path.realpath(sys.argv[2])
1122
1123         if os.path.commonprefix([data_path, art_path]) != data_path:
1124                 print "Error: article (%s) must be inside data_path (%s)" % \
1125                                 (art_path, data_path)
1126                 return 1
1127         art_path = art_path[len(data_path)+1:]
1128
1129         db_filename = os.path.join(data_path, 'db')
1130         if not os.path.isfile(db_filename):
1131                 open(db_filename, 'w').write('')
1132         db = ArticleDB(db_filename)
1133
1134         if cmd == 'add':
1135                 article = Article(art_path, datetime.datetime.now(),
1136                                         datetime.datetime.now())
1137                 for a in db.articles:
1138                         if a == article:
1139                                 print 'Error: article already exists'
1140                                 return 1
1141                 db.articles.append(article)
1142                 db.save()
1143                 if enable_comments:
1144                         comment_dir = os.path.join(comments_path, article.uuid)
1145                         try:
1146                                 os.mkdir(comment_dir, 0775)
1147                         except OSError, e:
1148                                 if e.errno != errno.EEXIST:
1149                                         print "Error: can't create comments " \
1150                                                 "directory %s (%s)" \
1151                                                         % (comment_dir, e)
1152                                 # otherwise is probably a removed and re-added
1153                                 # article
1154         elif cmd == 'rm':
1155                 article = Article(art_path)
1156                 for a in db.articles:
1157                         if a == article:
1158                                 break
1159                 else:
1160                         print "Error: no such article"
1161                         return 1
1162                 if enable_comments:
1163                         r = raw_input('Remove comments [y/N]? ')
1164                 db.articles.remove(a)
1165                 db.save()
1166                 if enable_comments and r.lower() == 'y':
1167                         shutil.rmtree(os.path.join(comments_path, a.uuid))
1168         elif cmd == 'update':
1169                 article = Article(art_path)
1170                 for a in db.articles:
1171                         if a == article:
1172                                 break
1173                 else:
1174                         print "Error: no such article"
1175                         return 1
1176                 a.updated = datetime.datetime.now()
1177                 db.save()
1178         else:
1179                 usage()
1180                 return 1
1181
1182         return 0
1183
1184
1185 if os.environ.has_key('GATEWAY_INTERFACE'):
1186         handle_cgi()
1187 else:
1188         sys.exit(handle_cmd())
1189
1190