#!/usr/bin/env python2.2 # Copyright (C) 2004 Paul Harrison # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA """ Aether - web/blog script Aether is a script for managing blogs and web pages entirely from the web. Design goals: The script should readable. All operations should be trivial, and part of the usual interface: * Editing pages * Creating pages and deleting pages * Renaming pages * Attaching files and deleting files The interface itself should be minimal and functional. "Features" are to be minimized: * No login, no cookies * All admin functions on the page itself (elegantly and discretely) * No secret interfaces, no codes to remember The script should handle anything that is thrown at it: * Unicode in pages * Unicode page names * Page and file names containing < > & " and whitespace Aether is based around an extensible markup language (not XML), which is translated to HTML on the fly. Features such as blogging are implemented as markup functions. Change log: 1.1 - 19 March 2004 Initial release. 1.2 - 29 March 2004 Support for ErrorDocument, based on a patch by Neil Brown. Simplified security model (old model unnecessary given ErrorDocument, just use multiple copies of script) _code.py now evalfile'd not imported More comments. Note: remove "import script" line from _code.py when upgrading from 1.1 1.3 - Locking added. Page content sent before sidebars (using CSS tomfoolery). [left] [right] tags behave differently, [top] [bottom] tags added. "standard_header" page no longer used, instead use page called "default" (header may now be set using the [top] tag). Allow use of variables before they are set (eg [_insert_later] tag). [toc] table of contents. Style-sheet may be modified using the [style] tag (eg in "default" page). """ __version__ = '1.3' # TODO: does attaching files with unicode filenames work? # (mozilla crashes!) # TODO: pages with names containing "?" (!) # Assumptions: # Unix-style paths, / # Unicode text # Unicode filename # Filenames beginning with "_" are special. # Attached files: # Separate attachment directory, attached files served directly by apache # though managed by Aether. import sys, os, time, codecs, string, cgi, re, StringIO, traceback, random from os import path import cgitb cgitb.enable() class Error: def __init__(self, message): self.message = message def __repr__(self): return 'Error: ' + self.message this_module = sys.modules[__name__] # Security -------------------------------------------------------------------- # In _code.py: password = u' ... ' # or: password = None allow_attachments = True # Paths and urls -------------------------------------------------------------- # host_url is http:// # # data_dir contains pages managed by Aether, and _code.py # host_url+page_base is the url of the pages managed by Aether # # file_dir contains files attached to pages # host_url+file_base is the url of file_dir # (attached files are served by Apache directly) # # host_url+script_base is the url of the Aether script script_filename = path.join(os.getcwd().decode('utf-8'), sys.argv[0].decode('utf-8')) data_dir = script_filename + u'-data' file_dir = script_filename + u'-files' host_url = 'http://' + os.environ.get('HTTP_HOST','').decode('utf-8') script_base = os.environ.get('SCRIPT_NAME','').decode('utf-8') file_base = script_base + u'-files' if 'AETHER_ROOT' in os.environ: # Script being used with Apache's ErrorDocument feature page_base = os.environ.get('AETHER_ROOT','').decode('utf-8') else: page_base = os.environ.get('SCRIPT_NAME','').decode('utf-8') while page_base[-1:] == u'/': page_base = page_base[:-1] # Hack so cgi module finds query string when serving error document if 'REDIRECT_QUERY_STRING' in os.environ: os.environ['QUERY_STRING'] = os.environ['REDIRECT_QUERY_STRING'] def get_request(): """ Which page was requested? Made slightly complicated by support for Apache's ErrorDocument feature. """ if 'REDIRECT_URL' in os.environ: request = os.environ['REDIRECT_URL'].decode('utf-8') else: request = os.environ['SCRIPT_NAME'].decode('utf-8') + \ os.environ.get('PATH_INFO','').decode('utf-8') if (request+u'/')[:len(page_base)+1] != page_base+u'/': raise Error('AETHER_ROOT incorrect in .htaccess?') request = request[len(page_base):] return request def name_to_url(name): """ URL of page named """ return host_url + page_base + u'/' + name def attachment_to_url(name, filename): """ URL of file attached to page """ return host_url + file_base + u'/' + name + u'/' + filename def make_form(content, method=u'post', extra_options=u'', **hiddens): """ Construct an HTML form. """ result = u'
' for key in hiddens: result += u'' result += content + u'
' return result # Unicode hacks -------------------------------------------------------------- # Encode various parameters in utf-8 before passing to standard functions. def path_exists(filename): return path.exists(filename.encode('utf-8')) def path_isdir(filename): return path.isdir(filename.encode('utf-8')) def path_isfile(filename): return path.isfile(filename.encode('utf-8')) def os_listdir(filename): result = os.listdir(filename.encode('utf-8')) for i in xrange(len(result)): result[i] = result[i].decode('utf-8') return result def os_unlink(filename): os.unlink(filename.encode('utf-8')) def os_rename(filename, new_filename): os.rename(filename.encode('utf-8'), new_filename.encode('utf-8')) def os_mkdir(dir): os.mkdir(dir.encode('utf-8')) def os_rmdir(dir): os.rmdir(dir.encode('utf-8')) def open(filename, mode): return __builtins__.open(filename.encode('utf-8'), mode) def codecs_open(filename, mode): return codecs.open(filename.encode('utf-8'), mode, 'utf-8') # Locking --------------------------------------------------------------------- def lock(): """ Acquire lock for access to files. Lock can not be held for more than ten seconds. Do not communicate with Apache while the lock is held, as network traffic can take arbitrary time. """ filename = (data_dir+u'/_lock').encode('utf-8') try: begin = os.stat(filename).st_mtime if abs(time.time()-begin) > 10.0: os.utime(filename, None) return # Note: race condition between stat and utime. except OSError: pass for i in xrange(100): try: os.mkdir(filename) return except OSError: pass time.sleep(0.1) raise Error('System overloaded') def unlock(): """ Release lock on access to files. """ os_rmdir(data_dir+u'/_lock') # File reading/writing ------------------------------------------------------- def check_for_filename_hack(filename): """ Check for attempt to write to parent directory or special file. """ if len(filename) < 1 or \ filename[:1] in (u'.', u'_') or \ u'/' in filename: raise Error('Hack attempt: '+repr(filename)) def check_for_path_hack(path): """ Check for attempt to write to parent directory or special file. """ if path[:1] in (u'.',u'_',u'/') or \ path.find(u'//') != -1 or \ path.find(u'/.') != -1 or \ path.find(u'/_') != -1: raise Error('Hack attempt: '+repr(path)) def ensure_dir_exists(dir): """ Ensure a directory exists, move file out of way if necessary. (rename to dir/_index). """ if path_isdir(dir): return if path_isfile(dir): # TODO: do nicerly somehow os_mkdir(dir+u'-TEMP') os_rename(dir, path.join(dir+u'-TEMP',u'_index')) os_rename(dir+u'-TEMP', dir) return ensure_dir_exists(path.dirname(dir)) os_mkdir(dir) def correct_case(name): """ Modify case in to match existing page or directory. (to let page URLs not be case sensitive) """ if not name: return name if path_exists(path.join(data_dir,name)): return name dir, name = path.split(name) dir = correct_case(dir) names = list_names(dir) for this_name, full_name in names: if this_name.lower() == name.lower(): name = this_name break return path.join(dir,name) def _get_filename(name, zone=u'', append__index=True): check_for_path_hack(name) filename = path.normpath(path.join(data_dir,zone,name)) if append__index and path_isdir(filename): filename = path.join(filename, '_index') return filename def load(name, zone=u''): """ Load a page. Returns contents of page file as unicode string. """ filename = _get_filename(name, zone) if not path_isfile(filename): raise Error('File does not exist') file = codecs_open(filename, 'rU') text = file.read() file.close() return text def save(text, name, zone=u''): """ Save text in a page file. """ filename = _get_filename(name, zone, False) if path_isdir(filename): # is it empty or not? if os_listdir(filename.encode('utf-8')): filename = path.join(filename,'_index') else: os_rmdir(filename) dir = path.dirname(filename) ensure_dir_exists(dir) file = codecs_open(filename, 'wb') file.write(text) file.close() def remove(name, zone=u''): """ Delete a page file. """ filename = _get_filename(name, zone) if not path_exists(filename): return os_unlink(filename) def exists(name, zone=u''): """ Does a page file exist for ? """ filename = _get_filename(name, zone) return path_exists(filename) def list_names(name): """ List contents of a directory. """ name = correct_case(name) filename = _get_filename(name, u'', False) if not path_isdir(filename): return [ ] files = os_listdir(filename) files.sort() # TODO: convert to unicode files = filter(lambda name: name[:1] not in ('.','_'), files) for i in xrange(len(files)): files[i] = (files[i], path.join(name,files[i])) return files # Time ------------------------------------------------------------------------ def now(): """ Return time in seconds since unix epoch, as an 11 digit decimal. """ return '%011d' % long(time.time()) def describe_time(t): """ Produce a human readable description of a time. Note: Aether ignores local time. All times are UTC. (as it is common for both readers and authors to be in timezones other than that of the server, local time has little meaning) """ tuple = time.gmtime(int(t)) return '%d %s %d, %d:%02d UTC'% \ (tuple[2], time.strftime('%B',tuple), tuple[0], tuple[3], tuple[4]) def w3c_time(t): """ Produce a W3C standard time string. """ tuple = time.gmtime(int(t)) return time.strftime('%Y-%m-%dT%H:%M:%SZ', tuple) # Quoting --------------------------------------------------------------------- html_quote_sequence = [ (u'&', u'&'), (u'<', u'<'), (u'>', u'>'), (u'"', u'"') ] html_unquote_sequence = [ (u'\n', u'


'), (u'\n', u'

'), (u'"', u'"'), (u'>', u'>'), (u'<', u'<'), (u'&', u'&') ] def quote_html(text, markup_newlines=False): """ Convert special HTML characters to &xyz; form. If , convert newlines into

tags, etc. """ for a, b in html_quote_sequence: text = text.replace(a, b) if markup_newlines: text = re.sub(ur'[ \t]*\r?\n',ur'\n', text) text = re.sub(ur'\n\n+', lambda match: u'


'*(len(match.group(0))-2)+u'\n

', text) return text def unquote_html(text): """ Precisely undo changes made by quote_html. """ for a, b in html_unquote_sequence: text = text.replace(b, a) return text def quote_markup(text): """ Escape special characters used by Aether markup language. """ text = re.sub(ur'([\\\[\]])', ur'\\\1', text) return text def unquote_markup(text): """ Precisely undo changes made by quote_markup. """ text = re.sub(ur'\\([\\\[\]])', ur'\1', text) return text def quote_paranoid(text): """ Convert utf-8 string to sequence of lower case English characters. """ text = text.encode('utf-8') result = '' for char in text: result += chr(ord('a') + ord(char)/16) +\ chr(ord('a') + ord(char)%16) return result def unquote_paranoid(text): """ Precisely undo the mapping performed by quote_paranoid. """ result = '' text = text.lower() for i in xrange(0,len(text),2): result += chr( (ord(text[i])-ord('a'))*16 + (ord(text[i+1])-ord('a')) ) return result.decode('utf-8') # HTML generation (markup) ---------------------------------------------------- # (you may replace style_sheet with a "" if you wish) style_sheet = u"""\ """ # Default options for page layout # (may be overridden in page itself, or by a page named "default") default = u"""\ [top [html

][_insert_html_later title][html

]] """ page_markup = u"""\ [_insert_html _top] [html
][_insert_html _page][html
] [html
][_insert_html _left][html
] [html ] [html
] [html
] [_insert_html _bottom] [html
][search-form][html ] [_insert_html _control_bar_extra] [edit-link] [link [url http://www.logarithmic.net/pfh/aether] \[\u00e6\]] [html
] """ def _split_command(thing): """ (markup helper function) """ # ugliness because everything in html match = re.search('(\s|\)', thing) if match == None: command = thing text = u'' else: command = thing[:match.start()] text = thing[match.end():] command = command.replace('-','_').lower() return command, text def markup(text, meta=None, outermost=True): """ Markup language translator. Markup language is less ugly than html, a la wiki. Returns html text, modifies meta if given. If the result will be used by another call to markup with the same meta, set outermost to False. (necessary to get postprocessing step right) Special characters: [ ] \ Escaping: \[ \] \\ [blah some text] results in a call to a function called markup_blah """ if meta is None: meta = { } chunks = re.split(ur'(\[|\]|\\\[|\\\]|\\\\)', text) stack = [ u'' ] context_stack = [ { } ] for chunk in chunks: if chunk == '[': stack.append(u'') context_stack.append({ }) elif chunk == ']' and len(stack) >= 2: thing = stack.pop(-1) thing_context = context_stack.pop(-1) command, text = _split_command(thing) if len(stack) >= 2: context = u'context_' + _split_command(stack[-1])[0] context = getattr(this_module,context,[ ]) else: context = [ ] if command in context: context_stack[-1][str(command)] = text text = u'' elif command + u'*' in context: context_stack[-1][str(command)] = \ context_stack[-1].get(str(command),[ ]) + [ text ] text = u'' else: try: text = getattr(this_module, 'markup_'+command)(text, meta, **thing_context) except: string_file = StringIO.StringIO() traceback.print_exc(file=string_file) text = u'
'+quote_html(string_file.getvalue())+u'
' stack[-1] = stack[-1] + text else: if chunk in (u'\\]',u'\\[',u'\\\\'): chunk = chunk[1] stack[-1] = stack[-1] + quote_html(chunk, True) # should only be one item on stack, but don't freak if there's more result = string.join(stack,u'') if outermost: for mark, callback in meta.get('_callbacks',[]): value = callback() result = result.replace(mark, value) return result _callback_counter = 0xe000 # Unicode private use area def make_callback_mark(meta, callback): """ Utility for markup_ functions: Return a special unicode character indicating that the result of callback() should be inserted here. The callback will be called *after* all markup has occured. This allows inserting variables that have not been set yet, and such. """ global _callback_counter mark = unichr(_callback_counter) _callback_counter += 1 meta['_callbacks'] = meta.get('_callbacks',[]) + [(mark, callback)] return mark def markup__insert(text, meta): return quote_html(meta.get(text.strip(), u'')) def markup__insert_html(text, meta): return meta.get(text.strip(), u'') def markup__insert_later(text, meta): def callback(): return quote_html(meta.get(text.strip(), u'')) return make_callback_mark(meta, callback) def markup__insert_html_later(text, meta): def callback(): return meta.get(text.strip(), u'') return make_callback_mark(meta, callback) def markup_html(text, meta): """ Insert some HTML tags. Example: [html hello ]""" return unquote_html(text) def markup_style(text, meta): """ Add to the HTML style sheet. Example: [style body { background: #88f } ] """ meta['_head_extra'] = meta.get('_head_extra',u'') + u'' return u'' def _resolve_url(text, meta, url, page, file, must_be_file=False): """ Helper for markup_link, markup_image url, page, file are html returns url as text, link-text as html""" text = text.strip() if page != None: page = page.strip() correct_page = correct_case( unquote_html(page) ) if file != None: file = file.strip() if url != None: url = url.strip() if url == None: if page != None and file != None: url = attachment_to_url(correct_page, unquote_html(file)) if not text: text = file elif file != None: url = attachment_to_url(meta['name'], unquote_html(file)) if not text: text = file elif page != None and not must_be_file: url = name_to_url(correct_page) if not text: text = page elif not must_be_file: url = unquote_html(text) if re.match(u'[A-Za-z]+:', url) is None: url = name_to_url( correct_case(url) ) else: raise Error('Nothing to link to') if not text: text = quote_html(url) return text, url context_link = ['url','page','file'] def markup_link(text, meta, url=None, page=None, file=None): """ Insert a link. Examples: [link somepage] (or [link [page somepage]] ) - link to page called somepage [link This is the page. [page somepage]] - link with text different to page name [link [file myfile.pdf]] - link to file attached to this page [link [page otherpage] [file otherfile.pdf]] - link to file attached to another page [link [url http://some.url.org]] (or [link http://some.url.org] ) - link to a url """ text, url = _resolve_url(text, meta, url, page, file) return u'' + text + '' context_image = ['url','page','file','title'] def markup_image(text, meta, url=None, page=None, file=None, title=None): """ Insert an image. Parameters as per [link]. """ text, url = _resolve_url(text, meta, url, page, file, True) if title == None: title = u'' else: title = u' title="' + quote_html(title) + '"' return u'' def markup_heading(text, meta): """ Heading. Example: [heading Introduction] """ meta['_headings'] = meta.get('_headings',[]) + [ text ] return u'

' + \ text + u'

' def markup_subheading(text, meta): """ Sub-heading. Example: [subheading Implementation details] """ return u'

'+text+u'

' def markup_toc(text, meta): """ Table of contents. List all [heading]s. """ def callback(): result = [ ] headings = meta.get('_headings',[ ]) for i in xrange(len(headings)): result.append( u'' + headings[i] + '' ) return string.join(result, u'
') return make_callback_mark(meta, callback) def markup_line(text, meta): """ Display text on separate line. Example: One must consider: [line 1. What?] [line 2. Where?] [line 3. Why?] """ return u'
'+text def markup_rule(text, meta): """ Draw a horizontal line. Example: [rule] """ return u'
'+text def markup_bullet(text, meta): """ Bullet point. Example: [bullet New nation] [bullet Civil war] [bullet Dedicate field] [bullet Dedicated to unfinished work] [bullet New birth of freedom] [bullet Government not perish] """ return u'
  • '+text+u'
' def markup_small(text, meta): """ Smaller font size. Example: [right [small It's an interesting fact that in... ]] """ return u''+text+u'' def markup_bold(text, meta): """ Bold text. Example: Well, that was [bold startling]! """ return u''+text+u'' def markup_mono(text, meta): """ Monospaced text. All formatting will be preserved. Example: [mono def markup_mono(text, meta): return u'
'+text+u'
' ] """ return u'
'+text+u'
' def markup_sans(text, meta): """ Sans-serif text. Example: [sans [link Home [page home]]] """ return u''+text+u'' def markup_indent(text, meta): """ Indent text. Example: Douglas Adams once wrote: [indent Anyone who is capable of getting themselves made President should on no account be allowed to do the job.] """ return u''+text+u'' def markup_itallic(text, meta): """ Itallic text. Example: A [itallic non-linear causal predictor] was used. """ return u''+text+u'' def markup_title(text, meta): """ Specify the title of the page. Example: [title Home] """ meta['title'] = text return u'' def markup_summary(text, meta): """ Mark a range of text as a summary or abstract. When used in a blog entry, only the summary will appear on the main blog page. This tag does not affect the appearance of the actual page. Example: [summary Python is a remarkable language.] It has a number of useful features. """ meta['summary'] = text return text def markup_entry(text, meta): """ Specify that this page is a blog entry. Only pages containing the [entry] tag will appear in a blog. Example: [entry] [title Thing story] A funny thing happened the other day. """ meta['class'] = 'entry' return text def _border_helper(border, text, meta, prepend=None, append=None): if prepend != None: meta[border] = text + meta.get(border, u'') elif append != None: meta[border] = meta.get(border, u'') + text else: meta[border] = text context_top = [ 'prepend', 'append' ] def markup_top(text, meta, **options): """ Specify text for top of page. Options as per [left]. """ _border_helper('_top', text, meta, **options) return u'' context_left = [ 'prepend', 'append' ] def markup_left(text, meta, **options): """ Specify text for left of page. By default, replaces any previously specified text, use the flags [prepend] or [append] to override this. I suggest using [left] as a menu bar for navigating your site. Examples: [left Menu bar...] [left [append] See also in this site...] [left [prepend] Table of contents...] """ _border_helper('_left', text, meta, **options) return u'' context_right = [ 'prepend', 'append' ] def markup_right(text, meta, **options): """ Specify text for right of page. Options as per [left]. I suggest using [right] for footnotes and links to related external links of your site. """ _border_helper('_right', text, meta, **options) return u'' context_bottom = [ 'prepend', 'append' ] def markup_bottom(text, meta, **options): """ Specify text for the bottom of page. Options as per [left]. I suggest using [bottom] for copyright notices and disclaimers. """ _border_helper('_bottom', text, meta, **options) return u'' context_include = ['default'] def markup_include(text, meta, default=u''): # Insert one page into another. # (i'm still thinking about how to do this well, eg inserting only part of # a page...) name = text.strip() old_name = meta.get('name', None) older_name = meta.get('outer_name', None) if old_name == None: if 'outer_name' in meta: del meta['outer_name'] else: meta['outer_name'] = old_name meta['name'] = name try: result = markup(load(name), meta, False) except Error: result = default if old_name == None: if 'name' in meta: del meta['name'] else: meta['name'] = old_name if older_name == None: if 'outer_name' in meta: del meta['outer_name'] else: meta['outer_name'] = older_name return result def markup_search_form(text, meta): return make_form( u'', 'get', action=u'search') def markup_edit_link(text, meta): if 'name' not in meta: return u'' if password: password_field = u'' else: password_field = u'' return make_form( u'' + password_field, action='edit', name=meta['name']) context_blog = [ 'location', 'author', 'email', 'tagline', 'length' ] def markup_blog(text, meta, location=u'', author=None, email=None, tagline=None, length='10'): """ Insert a blog. Optional parameters: author - Your name. email - Your email address. tagline - A short description of the blog. length - Number of entries to display on this page. location - Path to blog entries (defaults to current page, which is what you want unless you're doing something weird). Also provides an Atom XML feed, which should be detected automatically by news aggregators. You will be able to create new blog entries using the [new ___] item at the bottom of the page. Example: [title My blog] [blog [author Joe] [email joe@joe.joe] [tagline The life and times of Joe.] [length 10] ] """ location = location.strip() if not location: location = meta['name'] names = list_names(location) names.reverse() if meta.get('_blog_all'): length = 0 shorts_length = len(names) else: length = int(length.strip()) shorts_length = length*3 result = [ ] entries = [ ] shorts = [ ] n_entries = 0 latest = 0L for name, full_name in names: if not exists(full_name): continue # directory end = re.match('[0-9]*', name).end() if end == 0: continue entry_text = load(full_name) entry_meta = {'name' : full_name, 'outer_name' : meta['name']} entry_text = markup(entry_text, entry_meta) if entry_meta.get('class') != 'entry': continue entry_text = entry_meta.get('summary', entry_text) entry_time = long(name[:end]) latest = max(latest, entry_time) entry_title = entry_meta.get('title', 'Entry') if n_entries >= length and n_entries < shorts_length: if n_entries == length: result.append('

') short = ( u'
'+describe_time(entry_time)+u''+ u''+ entry_title+u'' ) result.append(short) shorts.append(short) elif n_entries < length: if 'summary' in entry_meta: link_text = 'read more...' else: link_text = '[permalink]' result.append( u'

'+describe_time(entry_time)+u''+ u''+entry_title+u''+ u'
'+entry_text+ u'

'+link_text+u'


' ) entries.append( u'\n' u'' + entry_title + u'\n' + u'' + w3c_time(entry_time) + u'\n' + u'' + w3c_time(entry_time) + u'\n' + u'' + quote_html(name_to_url(full_name)) + u'\n' + u'\n'+ u'' + quote_html(entry_text) + u'\n' + u'' ) n_entries += 1 result.append(u'

All older entries') entry_name = path.join(location,now()) meta['_blog_entries'] = entries meta['_blog_shorts'] = shorts #(for index page) meta['_blog_modified'] = latest if author: meta['_blog_author'] = unquote_html(author) if email: meta['_blog_email'] = unquote_html(email) if tagline: meta['_blog_tagline'] = unquote_html(tagline) meta['_head_extra'] = meta.get('_head_extra',u'') + ( u'' ) if password: password_field = u'' else: password_field = u'' meta['_control_bar_extra'] = meta.get('_control_bar_extra',u'') + ( u' [atom feed]   ' + make_form( u'' + password_field, action=u'edit', hastext=u'yes', text=u'[entry]\n[title ]\n\n', name=entry_name)) return string.join(result, u'') # Handlers ------------------------------------------------------------------- def make_http_page(text, meta=None, status='200'): """ Produce HTTP headers and page text to pass to Apache, given text in Aether markup language. Includes style sheet, sidebars, edit buttons, etc.""" if meta == None: meta = { } markup(default, meta, False) parts = meta.get('name',u'').split('/') for i in xrange(len(parts)): default_name = string.join(parts[:i]+[u'default'],'/') try: markup(load(default_name), meta, False) except Error: pass text = markup(text, meta, False) meta['_page'] = text text = markup(page_markup, meta, False) meta['_page'] = text meta['_style'] = style_sheet return u'Status: ' + status + u'\n' + \ u'Content-Type: text/html; charset=utf-8\n' + \ u'\n' + \ markup(u'[html ][_insert_html title]' u'[html ][_insert_html _style][_insert_html _head_extra]' u'[html ]' u'[_insert_html _page]' u'[html ]', meta) def make_redirect(dest): return u'Status: 302\n' + \ u'Location: '+name_to_url(dest)+u'\n\n' def make_404(name): return make_http_page(u'[title 404 - page does not exist]', {'name':name}, '404') def make_error(message, explanation=u''): return make_http_page(u'[title Problem: '+message+u']\n' + explanation) def handle_search(name, query): search_text = query.get('search',u'') search = search_text.lower().split() result = [ ] def helper(name): if exists(name): text = load(name).lower() for term in search: if text.find(term) == -1: break else: result.append(u'[line [link ' + (quote_markup(name) or u'/') + u'[page '+quote_markup(name)+']]]') names = list_names(name) for name, full_name in names: helper(full_name) helper('') if not result: result.append('Search found nothing, sorry.') if search: result.insert(0, u'[title Search results]\n\n') result.insert(1, u'Searching for: [bold ' + quote_markup(string.join(search,' ')) + u']\n\n') else: result.insert(0, u'[title Site map]\n\n') return make_http_page(string.join(result,u'')) def handle_atom(name, query): """ Output an Atom (XML) version of a blog. """ meta = {'name':name} markup(load(name), meta) author = u'' if '_blog_author' in meta: author += u'' + quote_html(meta['_blog_author']) + '' if '_blog_email' in meta: author += u'' + quote_html(meta['_blog_email']) + '' feed_metadata = u'' + quote_html(meta.get('title',name)) + u'\n' + \ u'' + w3c_time(meta['_blog_modified']) + u'\n' if '_blog_tagline' in meta: feed_metadata += u'' + quote_html(meta['_blog_tagline']) + u'\n' return (u'Content-Type: application/xml; charset=utf-8\n\n' + u'\n' + u'\n' + u'\n\n' + feed_metadata + u'' + author + u'\n' + string.join(meta.get('_blog_entries',[]),u'\n') + u'\n\n') def handle_all(name, query): """ Output an index of all blog entries in a blog. """ meta = {'name':name, '_blog_all':True} markup(load(name), meta) title = meta.get('title',u'Blog') + ' : all entries' return make_http_page( u'[_insert_html _html]', { '_html' : string.join(meta['_blog_shorts'],u''), 'name' : name, 'title' : title } ) def handle_manual(name, query): """ Output doc-strings of markup functions. """ commands = { } for key in globals(): if key[:7] == 'markup_': name = key[7:] doc = getattr(this_module,key).__doc__ if not doc: continue doc = doc.replace('\n ','\n') doc = quote_markup(doc.strip()) commands[name] = u'[heading \\[' + name + '\\]]\n' + \ u'[mono '+doc+']\n' list = commands.keys() list.sort() for i in xrange(len(list)): list[i] = commands[list[i]] return make_http_page(u"""\ [title Aether reference manual] Aether uses a simple markup language. Square brackets are used to tag parts of the text. For example: [indent [mono What a \[itallic lovely\] day for sitting indoors reading \[link \[url http://slashdot.org\] slashdot\]. ]] produces [indent What a [itallic lovely] day for sitting indoors reading [link [url http://slashdot.org] slashdot].] A tag may do as little as change the font, or as much as insert an entire blog into the text. If you know Python, you may add tags of your own. [right [small [bold Technicalities] If you wish to actually display a square bracket use "\\\\\\[" or "\\\\\\]" (and "\\\\\\\\" for "\\\\"). To use HTML directly, use the \[html\] tag. To modify the layout of the page title, or insert default sidebars, create a page called "default". Subdirectories may also have their own "default" page. The default default is [mono \[top \[html

\] \[_insert_html_later title\] \[html

\]\]] ]] The available tags are listed below. """ + string.join(list,u''), {'name':u''} ) def _continue_editing(query): return make_form( u'', action=u'edit', name=query.get('name',u''), password=query.get('password',u'') ) def help_handle_attach(name, query): # assumes password already checked! data = query['file'] filename = query['filename'] filename = path.basename(filename) check_for_filename_hack(filename) ensure_dir_exists(path.join(file_dir,name)) file = open(path.join(file_dir,name,filename), 'wb') file.write(data) file.close() return make_http_page(u"""\ [title [_insert _filename] uploaded] [link [file [_insert _filename]]] now attached to page [link [page [_insert name]]]. [_insert_html _continue]""", { 'name':name, '_filename':filename, '_continue':_continue_editing(query)}) def help_handle_delete(name, query): # assumes password already checked! deleted = [ ] for key in query: if query[key] != u'on' or \ key[:8] != 'checkbox': continue filename = unquote_paranoid(key[8:]) check_for_filename_hack(filename) os_unlink(path.join(file_dir,name,filename)) deleted.append(quote_html(filename)) if not deleted: return make_error(u'no files were selected for deletion') deleted = '

'+string.join(deleted,'

') return make_http_page(u"""\ [title Deleted] Deleted: [_insert_html _deleted] [_insert_html _continue]""", { 'name':name, '_deleted':deleted, '_continue':_continue_editing(query)}) def handle_edit(_ignore_, query): """ Handle everything to do with editing pages and attaching files. """ name = query.get('name', u'') name = correct_case(name) if 'hasnewname' in query: new_name = query.get('newname',u'') else: new_name = name if password and query.get('password',u'') != password: if query.get('password',u'') == u'': message = u'you need to give the password' else: message = u'that\'s not the password' return make_error(message, u'The password is given in the file:\n\n[mono ' + quote_markup(path.basename(data_dir)) + u'/_code.py]\n\n' + u'When you use ' + u'Aether for the first time, a random password is chosen. ' + u'You may either use that or edit _code.py.') if 'attach' in query: if not allow_attachments: raise Error('File attachment disabled') return help_handle_attach(name, query) if 'delete' in query: if not allow_attachments: raise Error('File attachment disabled') return help_handle_delete(name, query) if 'text' in query: text = query['text'] elif 'hastext' not in query and exists(name): text = load(name) else: text = u'' if 'save' in query: if new_name != name: if exists(new_name): return make_error(u'a page already exists with that name') remove(name) if text.strip(): save(text, new_name) return make_redirect(new_name) form = make_form( u'

New page name:
' + u'

New text:' + u'
' + u'

' + u' ', action=u'edit', password=password, name=name, hastext=u'yes', hasnewname=u'yes') if not allow_attachments or new_name != name: # TODO: attachments move with page? attachments = u'' else: dir = path.join(file_dir,name) if path_exists(dir): files = os_listdir(dir) else: files = [ ] files.sort() file_list = [ ] for filename in files: full_filename = path.join(dir, filename) if not path_isfile(full_filename): continue if len(file_list) % 2: color = '#dddddd' else: color = '#eeeeee' checkbox_name = 'checkbox'+quote_paranoid(filename) file_list.append( u'' + u'' + quote_html(filename) + u'' + u'') attachments = u'\n




' if file_list: attachments += u'Files attached to this page:\n' + \ make_form( u'
' + u'' + string.join(file_list, u'') + u'
' + u'' + u'' + u'

', action=u'edit',delete='1', name=name, password=password ) attachments += make_form( u'

' + u'Attach file:'+ u'
' + u' ' + u'', extra_options=u'enctype="multipart/form-data"', action=u'edit',attach=u'1', name=name, password=password ) return make_http_page(u"""\ [right [sans [link Aether reference manual [url [_insert _url]]]] ] Page [link [page [_insert _old_name]] "[_insert _old_name]"] [_insert_html _form] """ + text + u'\n[_insert_html _attachments]', { '_form' : form, '_attachments' : attachments, '_url' : host_url + script_base + u'?action=manual', '_old_name' : name, 'name' : new_name}) def perform_request(name, query): """ Locked part of main(). """ # root page shall have a slash on the end # (very pedantic!) if not name and not query: return make_redirect(u'') if name[:1] == u'/': # should always be true. name = name[1:] correct_name = correct_case(name) while correct_name[-1:] == '/': correct_name = correct_name[:-1] if name != correct_name: return make_redirect(correct_name) if query: action = query.get('action',u'').encode('utf-8') return getattr(this_module,'handle_'+action)(name, query) if not exists(name): return make_404(name) return make_http_page(load(name), {'name': name}) def main(): """ Decode data passed to script by Apache, Acquire file system lock, Perform requested action or get requested page or whatever, Release file system lock, Output result to Apache. """ fields = cgi.FieldStorage() query = { } for key in fields.keys(): if key != 'file': query[key] = fields.getfirst(key,'').decode('utf-8') if fields.has_key('file'): if password and password != query['password']: raise Error('Incorrect password') query['filename'] = fields['file'].filename.decode('utf-8') query['file'] = fields['file'].file.read() name = get_request() lock() try: output = perform_request(name, query) finally: unlock() sys.stdout.write(output.encode('utf-8')) if __name__ == '__main__': # Customization and configuration ---------------------------------------- if not path_exists(data_dir): os_mkdir(data_dir) if not path_exists(data_dir+u'/.htaccess'): file = open(data_dir+u'/.htaccess','wt') file.write('Deny from all\n') file.close() if not path_exists(data_dir+u'/_code.py'): file = open(data_dir+u'/_code.py','wt') file.write('# Insert custom python code here\n') file.write(u'\npassword = u\'') for i in xrange(5): file.write(chr( ord('a')+random.randrange(26) )) file.write(u'\'\n\n') file.close() # Allow imports from _code.py sys.path.insert(0, data_dir.encode('utf-8')) execfile((data_dir+u'/_code.py').encode('utf-8')) # Run -------------------------------------------------------------------- main()