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