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