#!/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

"""

__version__ = '1.2'

# TODO: locking
# TODO: does attaching files with unicode filenames work?
#       (mozilla crashes!)
# TODO: forbid use of some tags in wiki mode
# 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: pass

this_module = sys.modules[__name__]


# Security --------------------------------------------------------------------

# In _code.py: password = u' ... '
#          or: password = None

allow_attachments = True



# Paths and urls --------------------------------------------------------------

# host_url is http://<host name>
#
# 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 <name> """
    return host_url + page_base + u'/' + name


def attachment_to_url(name, filename):
    """ URL of file <filename> attached to page <name> """
    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'<form method=' + method + \
             u' accept-charset="utf-8" action="' + \
             quote_html(host_url+script_base) + u'" ' + \
             extra_options + u'>'

    for key in hiddens:
        result += u'<input type=hidden name=' + key + ' value="' + \
                  quote_html(hiddens[key]) + u'">'

    result += content + u'</form>'
    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 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')


def output(text):
    if isinstance(text, unicode):
        text = text.encode('utf-8')
    sys.stdout.write(text)



# 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


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


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 <name> 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, append__index=True):
    check_for_path_hack(name)

    filename = path.join(data_dir,name)

    if append__index and path_isdir(filename):
        filename = path.join(filename, '_index')

    return filename
    

def load(name):
    """ Load a page. Returns contents of page file as unicode string. """

    filename = _get_filename(name)

    if not path_isfile(filename):
        raise Error()

    file = codecs_open(filename, 'rU')
    text = file.read()
    file.close()

    return text


def save(text, name):
    """ Save text in a page file. """

    filename = _get_filename(name, False)

    if path_isdir(filename):
        # is it empty or not?
        if os_listdir(filename.encode('utf-8')):     
            filename = path.join(filename,'_index')
        else:
            os_unlink(filename)

    dir = path.dirname(filename)
    ensure_dir_exists(dir)

    file = codecs_open(filename, 'wb')
    file.write(text)
    file.close()


def remove(name):
    """ Delete a page file. """

    filename = _get_filename(name)

    if not path_exists(filename):
        return

    os_unlink(filename)


def exists(name):
    """ Does a page file exist for <name>? """

    filename = _get_filename(name)

    return path_exists(filename)


def list_names(name):
    """ List contents of a directory. """

    name = correct_case(name)
    filename = _get_filename(name, 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'&amp;'),
    (u'<', u'&lt;'),
    (u'>', u'&gt;'),
    (u'"', u'&quot;')
]

html_unquote_sequence = [
    (u'\n', u'<p><br>'),
    (u'\n', u'<p>'),
    (u'"', u'&quot;'),
    (u'>', u'&gt;'),
    (u'<', u'&lt;'),
    (u'&', u'&amp;')
]


def quote_html(text, markup_newlines=False):
    """ Convert special HTML characters to &xyz; form. 
        If <markup_newlines>, convert newlines into <p> 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'<p><br>'*(len(match.group(0))-2)+u'\n<p>', 
            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 "<link>" if you wish)
style_sheet = u"""\
<style><!--
input, textarea
{ border: solid 1px; border-color: #ccc }

input[type="submit"]
{ color: #00f; background: #ccc; }

h1, h2, h3, h4, h5, h6, span.entrytitle
{ font-family: Sans-Serif; font-weight: normal; color: #f80; 
  display: block;
  margin-top: 60px;
  margin-bottom: 20px;
  letter-spacing: 1px }

h1
{ margin-top: 30px }

a
{ text-decoration: none }

table, tr, td
{ border: 0px; padding 0px; margin: 0px }

form, pre
{ display: inline }

hr
{ height: 1px; border-style: none; background: #cccccc }

span.entrytime
{ float: right; font-family: Sans-Serif; font-size: 75%;
  color: #888 }

span.entrytitle
{ margin-top: 10px; margin-bottom: 5px; font-weight: bold }

a.entrylink
{ font-family: Sans-Serif }

span.indent
{ margin-left: 30px; margin-right: 30px; display: block }

#main
{ margin-left: 150px; margin-right: 200px }

#right
{ float: right; width: 150px }

#left
{ float: left; width: 100px }

#controlbar
{ font-family: Sans-serif;
  background: #fff; clear: both }

--></style>"""


page_markup = u"""\
[include standard_header [html <h1>][_insert_html title][html </h1>]]
[html <div id=left>][_insert_html _left][html </div>]
[html <div id=right>][_insert_html _right][html </div>]
[html <div id=main>][_insert_html _page][html </div>]




[html <div id=controlbar><table width=100%><tr>
<td>][search-form][html </td><td align=right>]
[_insert_html _control_bar_extra]
[edit-link] [link [url http://www.logarithmic.net/pfh/aether] \[\u00e6\]]
[html </td></tr></table></div>]
"""


def _split_command(thing):
    """ (markup helper function) """
    # ugliness because everything in html

    match = re.search('(\s|\<p\>)', 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):
    """ Markup language translator.
        Markup language is less ugly than html, a la wiki. 
        Returns html text, modifies meta if given

        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''
            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'<b><pre>'+quote_html(string_file.getvalue())+u'</pre></b>'
                
            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'')

    return result



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_html(text, meta):
    """ Insert some HTML tags.
    
        Example:
          [html <b>hello</b> ]"""
    return unquote_html(text)



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()
    
    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'<a href="' + quote_html(url) + u'">' + text + '</a>'


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'<img src="' + quote_html(url) + '"' + title + u'>'


def markup_heading(text, meta):
    """ Heading.

        Example:
           [heading Introduction] """
           
    return u'<h2>'+text+u'</h2>'


def markup_subheading(text, meta):
    """ Sub-heading.

        Example:
           [subheading Implementation details] """

    return u'<h3>'+text+u'</h3>'


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'<br>'+text


def markup_rule(text, meta):
    """ Draw a horizontal line.

        Example:
           [rule] """

    return u'<hr>'+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'<ul><li>'+text+u'</li></ul>'


def markup_small(text, meta):
    """ Smaller font size.

        Example:
           [right [small It's an interesting fact that in... ]] """
           
    return u'<font size=-2>'+text+u'</font>'


def markup_bold(text, meta):
    """ Bold text.

        Example:
           Well, that was [bold startling]! """
           
    return u'<b>'+text+u'</b>'


def markup_mono(text, meta):
    """ Monospaced text. All formatting will be preserved.
    
        Example:
           [mono
              def markup_mono(text, meta):
                  return u'<pre>'+text+u'</pre>'
           ]
           """
    return u'<pre>'+text+u'</pre>'


def markup_sans(text, meta):
    """ Sans-serif text.

        Example:
           [sans [link Home [page home]]] """

    return u'<font face="Sans-Serif">'+text+u'</font>'


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'<span class=indent>'+text+u'</span>'


def markup_itallic(text, meta):
    """ Itallic text.

        Example:
           A [itallic non-linear causal predictor] was used. """
           
    return u'<i>'+text+u'</i>'


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


context_left = [ 'top' ]
def markup_left(text, meta, top=None):
    """ Specify some text to appear to the left of the main body of text.
        Multiple uses of this tag will be concatenated together. Typically
        this would be used to display a site map or menu.

        Example:
           [left [link home]] """

    if top != None:
        meta['_left'] = text + meta.get('_left',u'')
    else:
        meta['_left'] = meta.get('_left',u'') + text
    return u''


context_right = [ 'top' ]
def markup_right(text, meta, top=None):
    """ Specify some text to appear to the right of the main body of text.
        Multiple uses of this tag will be concatenated together. Typically
        this would be used to display footnotes to the main text.

        Example:
           [right For more information on RSPP, see... ] """

    if top != None:
        meta['_right'] = text + meta.get('_right',u'')
    else:
        meta['_right'] = meta.get('_right',u'') + text
    return u''


def markup_include(text, meta):
    # Insert one page into another.
    # (i'm still thinking about how to do this well, eg inserting only part of
    #  a page...)

    splitted = text.split(None, 1)
    name = unquote_html(splitted[0])
    if len(splitted) > 1:
        failover_text = splitted[1]
    else:
        failover_text = u''

    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)
    except Error:
        result = failover_text

    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'<input name=search><input type=submit name=submit value=search>', 
        'get', action=u'search')


def markup_edit_link(text, meta):
    if 'name' not in meta:
        return u''

    if password:
        password_field = u'<input type=password name=password size=3>'
    else:
        password_field = u''

    return make_form(
            u'<input type=submit name=submit value=edit>' +
            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('<p>')

            short = (
                u'<br><span class="entrytime">'+describe_time(entry_time)+u'</span>'+
                u'<a href="'+quote_html(name_to_url(full_name))+'">'+
                entry_title+u'</a>'
            )
            
            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'<p><span class=entrytime>'+describe_time(entry_time)+u'</span>'+
                u'<span class=entrytitle>'+entry_title+u'</span>'+
                u'<br>'+entry_text+
                u'<p><a class=entrylink href="'+quote_html(name_to_url(full_name))+u'">'+link_text+u'</a><p><br>'
            )

            entries.append(
                u'<entry>\n'
                u'<title>' + entry_title + u'</title>\n' +
                u'<issued>' + w3c_time(entry_time) + u'</issued>\n' +
                u'<modified>' + w3c_time(entry_time) + u'</modified>\n' +
                u'<id>' + quote_html(name_to_url(full_name)) + u'</id>\n' +
                u'<link rel="alternate" type="text/html" href="' + 
                   quote_html(name_to_url(full_name)) + u'"/>\n'+
                u'<content type="text/html" mode="escaped">' +
                   quote_html(entry_text) + u'</content>\n' +
                u'</entry>'
            )

        n_entries += 1

    result.append(u'<p><a href="' + 
                  quote_html(name_to_url(meta['name'])) + 
                  u'?action=all">All older entries</a>')

    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'<link rel="alternate" type="application/atom+xml" title="Atom feed" href="' +
                  quote_html(name_to_url(meta['name'])+u'?action=atom') + u'">' )
                    
                    
    if password:
        password_field = u'<input type=password name=password size=3>'
    else:
        password_field = u''

    meta['_control_bar_extra'] = meta.get('_control_bar_extra',u'') + (
                  u' <a href="' + 
                  quote_html(name_to_url(meta['name'])+u'?action=atom') + 
                  u'">[atom feed]</a> &nbsp; ' +
                  make_form(
                    u'<input type=submit name=submit value=new>' +
                    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 = { }

    text = markup(text, meta)

    meta['_page'] = text
    text = markup(page_markup, meta)

    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 <html><head><title>][_insert_html title]'
                  u'[html </title>][_insert_html _style][_insert_html _head_extra]'
                  u'[html </head><body>]'
                  u'[_insert_html _page]'
                  u'[html </body></html>]', meta)


def handle_redirect(dest):
    output(u'Status: 302\n' +
           u'Location: '+name_to_url(dest)+u'\n\n')


def handle_404(name):
    output(make_http_page(u'[title 404 - page does not exist]', {'name':name}, '404'))


def handle_error(message, explanation=u''):
    output(make_http_page(u'[title Problem: '+message+u']\n' + explanation))


def handle_search(search_text):
    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')

    output(make_http_page(string.join(result,u'')))


def handle_atom(name):
    """ Output an Atom (XML) version of a blog. """

    name = correct_case(name)
    meta = {'name':name}
    markup(load(name), meta)

    author = u''
    if '_blog_author' in meta:
        author += u'<name>' + quote_html(meta['_blog_author']) + '</name>'
    if '_blog_email' in meta:
        author += u'<email>' + quote_html(meta['_blog_email']) + '</email>'

    feed_metadata = u'<title>' + quote_html(meta.get('title',name)) + u'</title>\n' + \
                    u'<modified>' + w3c_time(meta['_blog_modified']) + u'</modified>\n'
    if '_blog_tagline' in meta:
        feed_metadata += u'<tagline>' + quote_html(meta['_blog_tagline']) + u'</tagline>\n'

    output(u'Content-Type: application/xml; charset=utf-8\n\n' +
           u'<?xml version="1.0" encoding="utf-8"?>\n' +
           u'<feed version="0.3" xmlns="http://purl.org/atom/ns#">\n' +
           u'<link rel="alternate" type="text/html" href="' + 
              quote_html(name_to_url(name)) + u'"/>\n\n' +
           feed_metadata +
           u'<author>' + author + u'</author>\n' +
           string.join(meta.get('_blog_entries',[]),u'\n') +
           u'\n</feed>\n')


def handle_all(name):
    """ Output an index of all blog entries in a blog. """

    name = correct_case(name)
    meta = {'name':name, '_blog_all':True}
    markup(load(name), meta)

    title = meta.get('title',u'Blog') + ' : all entries'

    output(make_http_page( u'[_insert_html _html]',
        { '_html' : string.join(meta['_blog_shorts'],u''),
          'name' : name, 
          'title' : title } ))


def handle_manual():
    """ 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]]

    output(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 "standard_header". 
(sidebars can be created using \[left\] and \[right\].)
The default contents of "standard_header" are

[mono   \[html <h1>\]
  \[_insert title\]
  \[html </h1>\]]


]]

The available tags are listed below.
""" + string.join(list,u''), {'name':u''} ))
    

def _continue_editing(query):
    return make_form(
            u'<input name=submit type=submit value="Continue editing page">',
            action=u'edit', name=query.get('name',u''), 
            password=query.get('password',u'') )


def handle_attach(name, query, filename, data):
    # assumes password already checked!

    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()

    output(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 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:
        handle_error(u'no files were selected for deletion')
        return

    deleted = '<p>'+string.join(deleted,'<p>')

    output(make_http_page(u"""\
[title Deleted]

Deleted:

[_insert_html _deleted]
    
    
[_insert_html _continue]""", 
        { 'name':name, '_deleted':deleted, 
          '_continue':_continue_editing(query)}))


def handle_edit(query, fields):
    """ 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'

        handle_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.')
        return
    
    if query.get('action') == u'attach':
        if not allow_attachments:
            raise Error()

        handle_attach(name, query, fields['file'].filename.decode('utf-8'), 
            fields['file'].file.read())
        return

    if query.get('action') == u'delete':
        if not allow_attachments:
            raise Error()

        handle_delete(name, query)
        return

    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):
                handle_error(u'a page already exists with that name')
                return

        remove(name)

        if text.strip():
            save(text, new_name)

        handle_redirect(new_name)
        return

    form = make_form(
        u'<p>New page name:<br><input name=newname value="'+
        quote_html(new_name) + u'" size=40 style="width: 100%">' +
        u'<p>New text:' +
        u'<br><textarea name=text rows=25 cols=80 style="width: 100%">' +
        quote_html(text) +
        u'</textarea>' +
        u'<p><input type=submit name=preview value="Preview changes">' + 
        u' <input type=submit name=save value="Save changes">',
        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'<tr bgcolor='+color+'><td>' + 
                u'<a href="' + quote_html(attachment_to_url(name,filename)) + 
                u'">' + quote_html(filename) + u'</a>' +
                u'</td><td align=right><input name=' + checkbox_name + 
                ' type=checkbox></td></tr>')
        
        attachments = u'\n<p><br><p><br><hr>'
            
        if file_list:
            attachments += u'<b>Files attached to this page:</b>\n' + \
                make_form(
                u'<form method=post accept-charset="utf-8">' +
                u'<table width=100% cellspacing=0 cellpadding=5>' +
                string.join(file_list, u'') +
                u'<tr><td>' +
                u'<td align=right>' +
                u'<input name=submit type=submit value="Delete selected files">' +
                u'</td></table></form><p>',
                action=u'delete', name=name, password=password
                )

        attachments += make_form(
            u'<form method=post accept-charset="utf-8" enctype="multipart/form-data">' +
            u'Attach file:'+
            u'<br>' +
            u'<input type=file name=file> ' +
            u'<input name=submit type=submit value="Attach file">',
            extra_options=u'enctype="multipart/form-data"',
            action=u'attach', name=name, password=password
            )
 
    output(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 main():
    """ Decode data passed to script by Apache, then output the appropriate 
        page or invoke appropriate handler. """

    fields = cgi.FieldStorage()
    query = { }
    for key in fields.keys():
        if key != 'file':
            query[key] = fields.getfirst(key,'').decode('utf-8')

    name = get_request()

    # root page shall have a slash on the end
    # (very pedantic!)
    if not name and not query:
        handle_redirect(u'')
        return

    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:
        handle_redirect(correct_name)
        return

    if query:
        action = query.get('action',u'')

        if action == u'search':
            handle_search(query.get('search',u''))
        elif action == u'atom':
            handle_atom(name)
        elif action == u'all':
            handle_all(name)
        elif action == u'manual':
            handle_manual()

        else:
            handle_edit(query, fields)

        return

    if not exists(name):
        handle_404(name)
        return

    output(make_http_page(load(name), {'name': name}))



if __name__ == '__main__':
    # Customization and configuration ----------------------------------------

    ensure_dir_exists(data_dir)

    if not path_isfile(data_dir+u'/.htaccess'):
        file = open(data_dir+u'/.htaccess','wt')
        file.write('Deny from all\n')
        file.close()

    if not path_isfile(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()


