Merge branch 'master' of ssh://trinity.codewiz.org/~/public_html/wiki/git/geekigeeki
authorBernie Innocenti <bernie@codewiz.org>
Wed, 19 Aug 2009 10:07:05 +0000 (12:07 +0200)
committerBernie Innocenti <bernie@codewiz.org>
Wed, 19 Aug 2009 10:07:05 +0000 (12:07 +0200)
Conflicts:
geekigeeki.py

1  2 
geekigeeki.conf.py
geekigeeki.py

diff --combined geekigeeki.conf.py
index d08db5206801b8cb29954e26e42ed4f42dd9de6b,773f5373480631231681363e987887a191b40e30..5c5d681a00b3095f62f7ae56cd333ec3cf65302f
@@@ -7,12 -7,9 +7,12 @@@ site_name = 'Codewiz
  # It can be set to an empty string or a text message
  site_icon = 'sys/favicon16x16.png'
  
 -# set to None for read-only sites, leave empty ('') to allow anonymous edits
 +# set to None for read-only sites, or
 +# leave empty ('') to allow anonymous edits
  # otherwise, set to a URL that requires authentication
  privileged_url = 'https://www.develer.com/~bernie/wiki'
 +#privileged_url = ''
 +#privileged_url = None
  
  data_dir = 'data'
  
@@@ -34,8 -31,14 +34,8 @@@ history_url = '../wikigit/wiki.git
  
  post_edit_hook = './post_edit_hook.sh'
  
- datetime_fmt = '%a %d %b %Y %I:%M %p'
+ datetime_fmt = '%a, %d %b %Y %H:%M:%S %Z'
  
 -# Is it possible to edit pages?
 -allow_edit = True
 -
 -# show hostnames?
 -show_hosts = True
 -
  # prefix before nonexistent link (usually '?')
  nonexist_pfx = ''
  
@@@ -43,4 -46,4 +43,4 @@@
  image_maxwidth = 400
  
  # Set to True for CGI var dump
 -debug_cgi = False
 +#debug_cgi = True
diff --combined geekigeeki.py
index 07365fdef0e0c1ffdf6aaa3d2c3660cbc4c569cc,c5b51096a1fbecfe242b189b8d84625b152cdb65..4569894255a653ab73d6312e6a7f62354808ed0d
@@@ -8,17 -8,23 +8,17 @@@
  # 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 3 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, see <http://www.gnu.org/licenses/>.
 +# (at your option) any later version.  You should have received a copy
 +# of the GNU General Public License along with this program.
 +# If not, see <http://www.gnu.org/licenses/>.
  
  __version__ = '4.0-' + '$Id$'[4:11]
  
- from time import clock
+ from time import clock, localtime, gmtime, strftime
  start_time = clock()
  title_done = False
  
 -import cgi, sys, os, re, errno, stat
 +import cgi, sys, os, re, errno, stat, glob
  
  image_ext = 'png|gif|jpg|jpeg|bmp|ico'
  video_ext = "ogg|ogv|oga" # Not supported by Firefox 3.5: mkv|mpg|mpeg|mp4|avi|asf|flv|wmv|qt
@@@ -30,9 -36,6 +30,9 @@@ url_re   = re.compile(r"[a-z]{3,8}://[^
  ext_re   = re.compile(r"\.([^\./]+)$")
  
  # CGI stuff ---------------------------------------------------------
 +def config_get(key, default=None):
 +    return globals().get(key, default)
 +
  def script_name():
      return os.environ.get('SCRIPT_NAME', '')
  
@@@ -44,7 -47,7 +44,7 @@@ def query_string()
          return os.environ.get('QUERY_STRING', '') or 'FrontPage'
  
  def privileged_path():
 -    return privileged_url or script_name()
 +    return config_get('privileged_url') or script_name()
  
  def remote_user():
      user = os.environ.get('REMOTE_USER', '')
@@@ -80,18 -83,18 +80,18 @@@ def permalink(s)
  def humanlink(s):
      return re.sub(r'(?:.*[/:]|)([^:/\.]+)(?:\.[^/:]+|)$', r'\1', s.replace('_', ' '))
  
 -# Split arg lists like "blah| blah blah| width=100 | align = center",
 +# Split arg lists like "blah|blah blah| width=100 | align = center",
  # return a list containing anonymous arguments and a map containing the named arguments
  def parse_args(s):
      args = []
 -    kwargs = {} 
 +    kvargs = {}
      for arg in s.strip('<[{}]>').split('|'):
          m = re.match('\s*(\w+)\s*=\s*(.+)\s*', arg)
          if m is not None:
 -            kwargs[m.group(1)] = m.group(2)
 +            kvargs[m.group(1)] = m.group(2)
          else:
              args.append(arg.strip())
 -    return (args, kwargs)
 +    return (args, kvargs)
  
  def url_args(kvargs):
      argv = []
      return ''
  
  # Formatting stuff --------------------------------------------------
- def emit_header(mime_type="text/html"):
+ def emit_header(mtime=None, mime_type="text/html"):
+     if mtime:
+         print("Last-Modified: " + strftime("%a, %d %b %Y %H:%M:%S GMT", gmtime(mtime)))
      print("Content-type: " + mime_type + "; charset=utf-8\n")
  
  def send_guru(msg_text, msg_type):
      print('<pre id="guru" onclick="this.style.display = \'none\'" class="' + msg_type + '">')
      if msg_type == 'error':
          print('    Software Failure.  Press left mouse button to continue.\n')
 -    print(msg_text)
 +    print(cgi.escape(msg_text))
      if msg_type == 'error':
          print '\n           Guru Meditation #DEADBEEF.ABADC0DE'
      print('</pre><script language="JavaScript" type="text/javascript" src="%s" defer="defer"></script>' \
          % relative_url('sys/GuruMeditation.js'))
  
- def send_title(name, text="Limbo", msg_text=None, msg_type='error', writable=False):
+ def send_title(name, text="Limbo", msg_text=None, msg_type='error', writable=False, mtime=None):
      global title_done
      if title_done: return
  
      # Head
-     emit_header()
+     emit_header(mtime)
      print('<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"')
      print('  "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">')
      print('<html xmlns="http://www.w3.org/1999/xhtml" lang="en" xml:lang="en">')
  
 -    print("<head><title>%s: %s</title>" % (site_name, text))
 +    print("<head><title>%s: %s</title>" % (config_get('site_name', "Unconfigured Wiki"), text))
      print(' <meta http-equiv="Content-Type" content="application/xhtml+xml; charset=utf-8" />')
      if not name:
          print(' <meta name="robots" content="noindex,nofollow" />')
  
 -    for meta in meta_urls:
 -        http_equiv, content = meta
 +    for http_equiv, content in config_get('meta_urls', {}):
          print(' <meta http-equiv="%s" content="%s" />' % (http_equiv, relative_url(content)))
  
 -    for link in link_urls:
 +    for link in config_get('link_urls', {}):
          rel, href = link
          print(' <link rel="%s" href="%s" />' % (rel, relative_url(href)))
  
 -    if name and writable and privileged_url is not None:
 +    editable = name and writable and config_get('privileged_url') is not None
 +    if editable:
          print(' <link rel="alternate" type="application/x-wiki" title="Edit this page" href="%s" />' \
              % (privileged_path() + '?a=edit&q=' + name))
  
 -    if history_url is not None:
 +    history = config_get('history_url')
 +    if history is not None:
          print(' <link rel="alternate" type="application/rss+xml" title="RSS" href="%s" />' \
 -            % relative_url(history_url + '?a=rss'))
 +            % relative_url(history + '?a=rss'))
  
      print('</head>')
  
      # Body
 -    if name and writable and privileged_url is not None:
 +    if editable:
          print('<body ondblclick="location.href=\'' + privileged_path() + '?a=edit&q=' + name + '\'">')
      else:
          print('<body>')
  
      # Navbar
      print('<div class="nav">')
 -    print link_tag('FrontPage', site_icon or 'Home', cssclass='navlink')
 +    print link_tag('FrontPage', config_get('site_icon', 'Home'), cssclass='navlink')
      if name:
          print('  <b>' + link_tag('?fullsearch=' + name, text, cssclass='navlink') + '</b> ')
      else:
          print('  <b>' + text + '</b> ')
      print(' | ' + link_tag('FindPage', 'Find Page', cssclass='navlink'))
 -    if 'history_url' in globals():
 -        print(' | <a href="' + relative_url(history_url) + '" class="navlink">Recent Changes</a>')
 +    if history:
 +        print(' | <a href="' + relative_url(history) + '" class="navlink">Recent Changes</a>')
          if name:
 -            print(' | <a href="' + relative_url(history_url + '?a=history;f=' + name) + '" class="navlink">Page History</a>')
 +            print(' | <a href="' + relative_url(history + '?a=history;f=' + name) + '" class="navlink">Page History</a>')
  
      if name:
          print(' | ' + link_tag(name + '?a=raw', 'Raw Text', cssclass='navlink'))
 -        if privileged_url is not None:
 +        if config_get('privileged_url') is not None:
              if writable:
                  print(' | ' + link_tag('?a=edit&q=' + name, 'Edit', cssclass='navlink', privileged=True))
              else:
@@@ -207,7 -211,7 +209,7 @@@ def link_tag(dest, text=None, privilege
          elif file_re.match(dest) and Page(dest).exists():
              link_class = 'wikilink'
          else:
 -            text = nonexist_pfx + text
 +            text = config_get('nonexist_pfx', '') + text
              link_class = 'nonexistent'
  
      # Prevent crawlers from following links potentially added by spammers or to generated pages
@@@ -221,22 -225,15 +223,22 @@@ def link_inline(name, descr=None, kvarg
      if not descr: descr = humanlink(name)
      url = relative_url(name)
      if video_re.match(name):
 -        return '<video controls="1" src="%s">Your browser does not support the HTML5 video tag</video>' % url
 +        return '<video controls="1" src="%s">Your browser does not support HTML5 video</video>' % url
      elif image_re.match(name):
          return '<a href="%s"><img border="0" src="%s" alt="%s" /></a>' % (url, url + url_args(kvargs), descr)
      elif file_re.match(name) and not ext_re.search(name): # FIXME: this guesses a wiki page
 -        return Page(name).send_naked()
 +        Page(name).send_naked(kvargs) # FIXME: we should return the page as a string rather than print it
 +        return ''
      else:
          return '<iframe width="100%%" scrolling="auto" frameborder="0" src="%s"><a href="%s">%s</a></iframe>' \
              % (url, url, name)
  
 +def link_inline_glob(pattern, descr=None, kvargs={}):
 +    s = ''
 +    for name in glob.glob(pattern):
 +        s += link_inline(name, descr, kvargs)
 +    return s
 +
  # Search ---------------------------------------------------
  
  def print_search_stats(hits, searched):
@@@ -326,30 -323,33 +328,32 @@@ def handle_get(pagename, form)
          else:
              send_httperror("403 Forbidden", pagename)
  
 -# Used by macros/WordIndex and macros/TitleIndex
 +# Used by sys/macros/WordIndex and sys/macros/TitleIndex
  def make_index_key():
      links = ['<a href="#%s">%s</a>' % (ch, ch) for ch in 'abcdefghijklmnopqrstuvwxyz']
      return '<p style="text-align: center">' + ' | '.join(links) + '</p>'
  
 -def page_list(dirname=None, re=None):
 -    if re is None:
 +def page_list(dirname=None, search_re=None):
 +    if search_re is None:
          # FIXME: WikiWord is too restrictive now!
 -        re = re.compile(r"^\b((([A-Z][a-z0-9]+){2,}/)*([A-Z][a-z0-9]+){2,})\b$")
 -    return sorted(filter(re.match, os.listdir(dirname or data_dir)))
 +        search_re = re.compile(r"^\b((([A-Z][a-z0-9]+){2,}/)*([A-Z][a-z0-9]+){2,})\b$")
 +    return sorted(filter(search_re.match, os.listdir(dirname or '.')))
  
- def send_footer(mod_string=None):
+ def send_footer(mtime=None):
 -    if globals().get('debug_cgi', False):
 +    if config_get('debug_cgi', False):
          cgi.print_arguments()
          cgi.print_form(form)
          cgi.print_environ()
-     link_inline("sys/footer", kvargs = { 'LAST_MODIFIED': mod_string })
 -        #FIXME link_inline("sys/footer")
 -    print('''
 -<div id="footer"><hr />
 -<p class="copyright">
 -<a rel="license" href="http://creativecommons.org/licenses/by-sa/3.0/"><img class="license" alt="Creative Commons License" src="%s" /></a>
 -<span class="benchmark">generated in %0.3fs</span> by <a href="http://www.codewiz.org/wiki/GeekiGeeki">GeekiGeeki</a> version %s
 -</p>
 -''' % (relative_url('sys/cc-by-sa.png'), clock() - start_time, __version__))
 -    if mtime:
 -        print('<p class="modified">last modified %s</p>' % strftime(datetime_fmt, localtime(mtime)))
 -    print('</div></body></html>')
++    link_inline("sys/footer", kvargs = {
++        'LAST_MODIFIED': strftime(config_get(datetime_fmt, '%a %d %b %Y %I:%M %p'), localtime(mtime))
++    })
 +    print("</body></html>")
 +
 +def _macro_ELAPSED_TIME(*args, **kvargs):
 +    return "%03f" % (clock() - start_time)
 +
 +def _macro_VERSION(*args, **kvargs):
 +    return __version__
  
  class WikiFormatter:
      """Object that turns Wiki markup into HTML.
      All formatting commands can be parsed one line at a time, though
      some state is carried over between lines.
      """
 -    def __init__(self, raw):
 +    def __init__(self, raw, kvargs=None):
          self.raw = raw
 +        self.kvargs = kvargs or {}
          self.h_level = 0
          self.in_pre = self.in_html = self.in_table = self.in_li = False
          self.in_header = True
  
      def _macro_repl(self, word):
          try:
 -            args, kwargs = parse_args(word)
 +            args, kvargs = parse_args(word)
 +            if args[0] in self.kvargs:
 +                return self.kvargs[args[0]]
              macro = globals().get('_macro_' + args[0])
              if not macro:
 -                exec(open("macros/" + name + ".py").read(), globals())
 -                macro = globals().get('_macro_' + name)
 -            return macro(*args, **kwargs)
 -        except Exception:
 -            msg = cgi.escape(word)
 +                exec(open("sys/macros/" + args[0] + ".py").read(), globals())
 +                macro = globals().get('_macro_' + args[0])
 +            return macro(*args, **kvargs)
 +        except Exception, e:
 +            msg = cgi.escape(word) + ": " + cgi.escape(e.message)
              if not self.in_html:
                  msg = '<strong class="error">' + msg + '</strong>'
              return msg
              # This double div nonsense works around a limitation of the HTML block model
              return '<div class="' + kvargs.get('class', 'thumb') + '">' \
                  + '<div class="innerthumb">' \
 -                + link_inline(name, descr, kvargs) \
 +                + link_inline_glob(name, descr, kvargs) \
                  + '<div class="caption">' + descr + '</div></div></div>'
          else:
 -            return link_inline(name, None, kvargs)
 +            return link_inline_glob(name, None, kvargs)
  
      def _html_repl(self, word):
          if not self.in_html and word.startswith('<div'): word = '</p>' + word
              if hit:
                  return getattr(self, '_' + rule + '_repl')(hit)
          else:
 -            raise "Can't handle match " + repr(match)
 +            raise Exception("Can't handle match " + repr(match))
  
      def print_html(self):
          print('<div class="wiki"><p>')
@@@ -627,20 -624,24 +631,24 @@@ class Page
          return re.sub('([a-z])([A-Z])', r'\1 \2', self.page_name)
  
      def _filename(self):
 -        return os.path.join(data_dir, self.page_name)
 +        return self.page_name
  
      def _tmp_filename(self):
 -        return os.path.join(data_dir, ('#' + self.page_name.replace('/','_') + '.' + str(os.getpid()) + '#'))
 +        return self.page_name + '.tmp' + str(os.getpid()) + '#'
  
-     def exists(self):
+     def _mtime(self):
          try:
-             os.stat(self._filename())
-             return True
+             return os.stat(self._filename()).st_mtime
          except OSError, err:
              if err.errno == errno.ENOENT:
-                 return False
+                 return None
              raise err
  
+     def exists(self):
+         if self._mtime():
+             return True
+         return False
      def get_raw_body(self, default=None):
          try:
              return open(self._filename(), 'rb').read()
   
          for filename in page_list(self._filename(), file_re):
              if image_re.match(filename):
 -                if image_maxwidth:
 -                    maxwidth_arg = ' | maxwidth=' + str(image_maxwidth)
 -                out += '{{' + self.page_name + '/' + filename + ' | ' + humanlink(filename) + maxwidth_arg + ' | class=thumbleft}}\n'
 +                maxwidth = config_get(image_maxwidth, '')
 +                if maxwidth:
 +                    maxwidth = ' | maxwidth=' + str(maxwidth)
 +                out += '{{' + self.page_name + '/' + filename + ' | ' + humanlink(filename) + maxwidth + ' | class=thumbleft}}\n'
              else:
                  out += ' * [[' + self.page_name + '/' + filename + ']]\n'
          return out
      def can_read(self):
          return self.can("read", True)
  
 -    def send_naked(self):
 +    def send_naked(self, kvargs=None):
          if self.can_read():
 -            WikiFormatter(self.get_raw_body()).print_html()
 +            WikiFormatter(self.get_raw_body(), kvargs).print_html()
          else:
              send_guru("Read access denied by ACLs", "notice")
  
              link_urls += [ [ "stylesheet", value ] ]
  
          send_title(self.page_name, self.split_title(),
-             msg_text=self.msg_text, msg_type=self.msg_type, writable=self.can_write())
+             msg_text=self.msg_text, msg_type=self.msg_type, writable=self.can_write(), mtime=self._mtime())
          self.send_naked()
-         send_footer(self._last_modified())
-     def _last_modified(self):
-         try:
-             from time import localtime, strftime
-             modtime = localtime(os.stat(self._filename())[stat.ST_MTIME])
-         except OSError, err:
-             if err.errno != errno.ENOENT:
-                 raise err
-             return None
-         return strftime(config_get(datetime_fmt, '%a %d %b %Y %I:%M %p'), modtime)
+         send_footer(mtime=self._mtime())
  
      def send_editor(self, preview=None):
          send_title(None, 'Edit ' + self.split_title(), msg_text=self.msg_text, msg_type=self.msg_type)
              send_guru("Write access denied by ACLs", "error")
              return
  
 -        filename = ''
 -        if 'file' in form:
 -            filename = form['file'].value
 -
 -        print(('<p><b>Editing ' + self.page_name
 -            + ' for ' + cgi.escape(remote_user())
 -            + ' from ' + cgi.escape(get_hostname(remote_host()))
 -            + '</b></p>'))
 -        print('<div class="editor"><form name="editform" method="post" enctype="multipart/form-data" action="%s">' % relative_url(self.page_name))
 -        print('<input type="hidden" name="a" value="edit" /><input type="hidden" name="q" value="' + self.page_name + '" />')
 -        print('<input type="input" id="editor" name="changelog" value="Edit page %s" accesskey="c" /><br />' % (self.page_name))
 -        print('<textarea wrap="off" spellcheck="true" id="editor" name="savetext" rows="17" cols="100" accesskey="e">%s</textarea>' \
 -            % cgi.escape(preview or self.get_raw_body(default='')))
 -        print('<label for="file" accesskey="u">Or Upload a file:</label> <input type="file" name="file" value="%s" />' % filename)
 -        print("""
 -            <br />
 -            <input type="submit" name="save" value="Save" accesskey="s">
 -            <input type="submit" name="preview" value="Preview" accesskey="p" />
 -            <input type="reset" value="Reset" />
 -            <input type="submit" name="cancel" value="Cancel" />
 -            <br />
 -            </form></div>
 -            <script language="javascript">
 -            <!--
 -            document.editform.savetext.focus()
 -            //-->
 -            </script>
 -            """)
 -        print("<p>" + link_tag('EditingTips') + "</p>")
 +        if preview is None:
 +            preview = self.get_raw_body(default='')
 +
 +        link_inline("sys/EditPage", kvargs = {
 +            'EDIT_BODY': cgi.escape(preview),
 +            #'EDIT_PREVIEW': WikiFormatter(preview).print_html(),
 +        })
 +
          if preview:
              print("<div class='preview'>")
              WikiFormatter(preview).print_html()
  
      def send_raw(self, mimetype='text/plain', args=[]):
          if not self.can_read():
-             send_title(None, msg_text='Read access denied by ACLs', msg_type='notice')
+             send_title(None, msg_text='Read access denied by ACLs', msg_type='notice', mtime=self._mtime())
              return
  
+         emit_header(self._mtime(), mimetype)
          if 'maxwidth' in args:
              import subprocess
-             emit_header(mimetype)
              sys.stdout.flush()
              subprocess.check_call(['gm', 'convert', self._filename(),
                  '-scale', args['maxwidth'].value + ' >', '-'])
          else:
              body = self.get_raw_body()
-             emit_header(mimetype)
              print(body)
  
      def _write_file(self, data):
  
          self._write_file(newdata)
          rc = 0
 -        if post_edit_hook:
 +        if config_get('post_edit_hook'):
              import subprocess
 -            cmd = [ post_edit_hook, data_dir + '/' + self.page_name, remote_user(), remote_host(), changelog]
 +            cmd = [
 +                config_get('post_edit_hook'),
 +                self.page_name, remote_user(),
 +                remote_host(), changelog ]
              child = subprocess.Popen(cmd, stdout=subprocess.PIPE, close_fds=True)
              output = child.stdout.read()
              rc = child.wait()
  
  try:
      exec(open("geekigeeki.conf.py").read())
 +    os.chdir(config_get('data_dir', 'data'))
      form = cgi.FieldStorage()
      action = form.getvalue('a', 'get')
      handler = globals().get('handle_' + action)