# Copyright Notice {{{ # WikiCyte - Awesome WWW Content Management System # Copyright (C) 2009 Jay Freeman (saurik) # Redistribution and use in source and binary # forms, with or without modification, are permitted # provided that the following conditions are met: # # 1. Redistributions of source code must retain the # above copyright notice, this list of conditions # and the following disclaimer. # 2. Redistributions in binary form must reproduce the # above copyright notice, this list of conditions # and the following disclaimer in the documentation # and/or other materials provided with the # distribution. # 3. The name of the author may not be used to endorse # or promote products derived from this software # without specific prior written permission. # # THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, # BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF # MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE # ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR BE # LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, # EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT # NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR # SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS # INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF # LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR # TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN # ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF # ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. # }}} # Import Statements {{{ import cgi, os, random, re, string, sys, urllib2, uuid from mod_python import apache, psp, Session, util from datetime import datetime from xml.sax import saxutils try: import wikidiff2 except: wikidiff2 = None cynfig = apache.import_module('~/cynfig.py') library = [os.path.dirname(__file__) + '/library'] markdown = apache.import_module('markdown/__init__', path=library) pytz = apache.import_module('pytz/__init__', path=library) smartypants = apache.import_module('smartypants', path=library) try : tidy = apache.import_module('tidy/__init__', path=library) except OSError: tidy = None try: markdown_extensions = cynfig.markdown_extensions except AttributeError: markdown_extensions = [] try: anonymous_users = cynfig.anonymous_users except AttributeError: anonymous_users = [] #markdown.logger.setLevel(markdown.CRITICAL) # }}} # Content Protocol Escapes {{{ def exml(val): return saxutils.escape(val) if val != None else '' def eatt(val): return saxutils.quoteattr(val) if val != None else '""' def euri(val): return exml(urllib2.quote(val)) if val != None else '' def etim(val): if cynfig.local_timezone != None: val = pytz.utc.localize(val) val = val.astimezone(cynfig.local_timezone) return val def ejss(val): # XXX: this is not correct return exml(val) def ecol(val): hsh = hash(val) r = ((hsh >> 16) & 255) * .95 g = ((hsh >> 8) & 255) * .95 b = ((hsh >> 0) & 255) * .95 return '%.2x%.2x%.2x' % (r, g, b) base = {'exml': exml, 'euri': euri, 'etim': etim, 'ecol': ecol} # }}} # Database Connections {{{ import psycopg2, psycopg2.pool from psycopg2.extras import DictCursor psycopg2.extensions.register_type(psycopg2.extensions.UNICODE) def getdb(): sql = psycopg2.connect(**cynfig.psycopg2_dsn) sql.set_client_encoding('UNICODE') return sql def putdb(sql): sql.close() def push(stmt, args, path=None): sql = getdb() try: csr = sql.cursor() try: csr.execute(stmt, args) if path != None: csr.execute("select currval(pg_get_serial_sequence(%s, %s))", path) rows = csr.fetchall() sql.commit() if path != None: return rows[0][0] finally: csr.close() finally: putdb(sql) def cursor(sql): return sql.cursor(cursor_factory=DictCursor) def pull(stmt, args): sql = getdb() try: csr = cursor(sql) try: csr.execute(stmt, args) rows = csr.fetchall() sql.commit() return rows finally: csr.close() finally: putdb(sql) def yank(stmt, args): rows = pull(stmt, args) return rows[0] if len(rows) != 0 else None # }}} # Proxy X-Real-IP Address {{{ # Getting someone's IP address isn't as easy as you'd # assume. While we can ask Apache for the IP address, # we may be behind a reverse proxy of some form (maybe # a cache or a load balancer). # # To get the real IP address, we can use the X-Real-IP # header, but we can't /trust/ that information as then # a user could claim to be anyone the want by just # supplying that header with their requests. # # We therefore have a section of the configuration file # for "known" proxies: these are proxies that are part # of the infrastructure setup by the administrator of # the webserver running WikiCyte. try: known_proxies = cynfig.known_proxies except AttributeError: known_proxies = [] def realip(req): host = req.connection.remote_ip if host in known_proxies: if 'X-Unique-ID' in req.headers_in: host = req.headers_in['X-Unique-ID'] return host # }}} def parm(form, name): val = form.getfirst(name) return str(val) if val != None else None def menu(req, uri, name, icon): uri = '/_' + euri(name) + uri req.write("""
  • """ % ( eatt(uri), eatt(name), ejss(uri), eatt('http://cache.saurik.com/silk/' + euri(icon) + '.png') )) def save(req, form, uri): rev = parm(form, 'from') then = parm(form, 'now') data = parm(form, 'data') if data != None: data = clean(data) then = datetime.fromtimestamp(float(then)) req.get_basic_auth_pw() user = req.user host = req.connection.remote_ip text = re.sub(r'{[^}]*}', r'', data) if data != None else None push(''' insert into "wikicyte"."page" ( "uri", "user", "host", "data", "search", "then", "from" ) values ( %(uri)s, %(user)s, %(host)s, %(data)s, to_tsvector(%(text)s), %(then)s, coalesce((select "id" from "wikicyte"."page" where "then" = %(then)s order by "when" desc limit 1), %(rev)s) ) ''', locals()) def head(req): req.content_type = "text/html; charset=UTF-8" req.send_http_header() def clean(data): return data.replace('\r\n', '\n').replace('\r', '\n') def edit(req, uri): req.get_basic_auth_pw() user = req.user if anon(user): return apache.HTTP_FORBIDDEN here = os.path.dirname(__file__) form = util.FieldStorage(req) if req.method == 'POST': save(req, form, uri) target = parm(form, 'target') if target == None: target = uri util.redirect(req, target) return apache.OK page = luri(uri) uri = exml(uri) target = parm(form, 'target') if target != None: target = exml(target) if page == None: rev = None data = '' else: rev = int(page['id']) data = exml(page['data']) head(req) data = clean(data) if data.startswith('\n'): data = '\n' + data data = data.encode('utf8') psp.PSP(req, filename=(here + '/wikicyte-edit.psp')).run(vars=dict({ 'uri': uri, 'rev': rev, 'data': data, 'target': target }, **base)) return apache.OK def history(req, uri): if wikidiff2 == None: return apache.HTTP_NOT_FOUND here = os.path.dirname(__file__) # XXX: naive datetime history = pull(''' select "id", "uri", "from", "user", "host", "when"::timestamp, "approved" from "wikicyte"."approved"(now()) where "uri" = %(uri)s or %(uri)s is null order by "when" desc nulls last ''', locals()) head(req) psp.PSP(req, filename=(here + '/wikicyte-history.psp')).run(vars=dict({ 'uri': uri, 'history': history }, **base)) return apache.OK def pid(id): return yank(''' select "data" from "wikicyte"."page" where "id" = %(id)s ''', locals()) def diff(req, lid, rid): if wikidiff2 == None: return apache.HTTP_NOT_FOUND here = os.path.dirname(__file__) lpage = pid(lid) rpage = pid(rid) head(req) ldata = lpage['data'].encode('utf8') rdata = rpage['data'].encode('utf8') diff = wikidiff2.wikidiff2_do_diff(ldata, rdata, 2) diff = diff.replace('">', '"> ') psp.PSP(req, filename=(here + '/wikicyte-diff.psp')).run(vars=dict({ 'diff': diff }, **base)) return apache.OK def time(req, uri): # XXX: implement using uri! head(req) # XXX: naive datetime when = yank(''' select "when"::timestamp from "wikicyte"."approved"(now()) where ("approved" is null or "approved") order by "when" desc nulls last limit 1 ''', locals()) if when == None: req.write('<unknown>') else: when = when['when'] when = etim(when) req.write(when.strftime('%B %d, %Y')) return apache.OK def prev(req, rev): page = lrev(rev) uri = page['uri'] return show(req, uri, page) def approve(req, rev, value): req.get_basic_auth_pw() user = req.user host = req.connection.remote_ip if anon(user): return apache.HTTP_FORBIDDEN push(''' insert into "wikicyte"."status" ( "page", "approved", "user", "host" ) values ( %(rev)s, %(value)s, %(user)s, %(host)s ) ''', locals()) return apache.OK def back(req, mode, uri): if mode == '': return puri(req, '/_' + uri) elif mode == 'approve': rev = uri.split('/') bool = {'null': None, 'true': True, 'false': False} return approve(req, int(rev[1]), bool[rev[2]]) elif mode == 'diff': rev = uri.split('/') return diff(req, int(rev[1]), int(rev[2])) elif mode == 'edit': return edit(req, uri) elif mode == 'history': return history(req, uri) elif mode == 'page': return prev(req, int(uri[1:])) elif mode == 'time': return time(req, uri) else: return apache.DECLINED def lrev(rev): return yank(""" select "uri", "when", "data" from "wikicyte"."page" where "id" = %(rev)s """, locals()) def luri(uri): return yank(""" select "id", "uri", "when", "data" from "wikicyte"."approved"(now()) where "uri" = %(uri)s and ("approved" is null or "approved") order by "when" desc nulls last limit 1 """, locals()) def puri(req, uri): # XXX: [-1:0] didn't work if req.uri[len(req.uri) - 1] != '/': path = req.uri + '/' user = req.user if anon(user) and luri(path) == None: return apache.HTTP_NOT_FOUND else: util.redirect(req, path) return apache.OK page = luri(uri) return show(req, uri, page) def anon(user): return user == None or user in anonymous_users def smrt(html): html = smartypants.smartyPants(html) #html = re.sub(r'([^-])--([^-])', r'\1—\2', html) # XXX: only squash space if previous #? html = html.replace(' 1/2 ', '½ ') html = html.replace(' x ', ' × ') html = html.replace('i.e.', 'i.e.') html = html.replace('e.g.', 'e.g.') html = re.sub(r'([0-9])-([0-9])', r'\1–\2', html) html = html.encode('utf8') if tidy != None: html = str(tidy.parseString(html, numeric_entities=1, output_xhtml=1, preserve_entities=1, show_body_only=1, char_encoding='utf8' )) return html def show(req, uri, page): form = util.FieldStorage(req) mode = parm(form, 'mode') xuri = exml(uri) root = req.document_root() req.get_basic_auth_pw() user = req.user if page != None: when = page['when'] data = page['data'] elif anon(user): return apache.HTTP_NOT_FOUND else: file = open('%(root)s/default.mdml' % locals(), 'r') try: when = None data = file.read() finally: file.close() md = markdown.Markdown(markdown_extensions + ['meta']) meta = {} crumbs = [] path = uri if path.endswith('/'): path = path[0:-1] path = path.split('/') for i in range(1, len(path)): suri = exml('/'.join(path[0:i]) + '/') last = luri(suri) if last == None: continue md.convert(last['data']) meta.update(md.Meta) if suri != '/' and 'title' in md.Meta: crumbs = [(suri, md.Meta['title'][0])] + crumbs html = data html = html.replace('\n', '\r\n') html = md.convert(html) html = smrt(html) meta.update(md.Meta) if len(crumbs) != 0 and not 'title' in md.Meta: crumbs = crumbs[:-1] head(req) cynfig.before(req) if when != None: when = when.strftime('%B %d, %Y') cyte(req, meta, base=req.construct_url(xuri), href=uri, user=user, time=when, mode=mode) req.write(html) req.write('''''') for crumb in crumbs: req.write('''%s''' % (exml(crumb[0]), exml(crumb[1]))) if not anon(user): req.write('''''') req.write('''''') return apache.OK def cyte(req, meta, **extra): req.write('''''') req.write('''
    ''') def search(req): form = util.FieldStorage(req) query = parm(form, 'q') host = realip(req) if query == None: # XXX: implement this? return apache.OK push(''' insert into "wikicyte"."search" ( "query", "host" ) values ( %(query)s, %(host)s ) ''', locals()) terms = query.replace('"', '').split(' ') head(req) cynfig.before(req) md = markdown.Markdown(markdown_extensions + ['meta']) # XXX: this is stupid page = luri('/_search/') if page == None: meta = {} else: md.convert(page['data']) meta = dict(md.Meta) title = 'Search results: ' + query if 'title' in meta: meta['title'][0] = title else: meta['title'] = [title] cyte(req, meta, search=query) pages = pull(''' select "current"."uri", "current"."data", ts_rank_cd("current"."search", "query") as "rank" from "wikicyte"."current"(now()) as "current", plainto_tsquery(%(query)s) as "query" where "current"."search" @@ "query" and substring("current"."uri" from 1 for 2) != '/_' order by "rank" desc ''', locals()) rx = re.compile('(' + '|'.join(terms) + ')', re.IGNORECASE) rm = re.compile(r'{[^}]*}|<[^>]*>|\([^\)]*/[^\)]*\)|[][*#]') html = [] if len(pages) == 0: html.append("

    No results found.

    ") else: html.append('''''') html = ''.join(html) html = smrt(html) req.write(html) req.write('''
    ''') req.write('''Search results: %s''' % (exml(query),)) req.write('''''') return apache.OK def sitemap(req): head(req) cynfig.before(req) md = markdown.Markdown(markdown_extensions + ['meta']) page = luri('/_sitemap/') if page == None: meta = {} else: md.convert(page['data']) meta = dict(md.Meta) if 'title' in meta: meta['title'][0] = 'Site map' else: meta['title'] = ['Site map'] cyte(req, meta) pages = pull(''' select "current"."uri", "current"."data" from "wikicyte"."current"(now()) as "current" where substring("uri" from 1 for 2) != '/_' and "uri" != '/' order by "current"."uri" ''', locals()) uris = ['/'] html = [] html.append('''''') html.append('''
  • %(name)s
  • ''') html.append('''''') html = ''.join(html) html = smrt(html) req.write(html) req.write('''''') req.write('''''') return apache.OK def query(req): req.get_basic_auth_pw() user = req.user host = realip(req) if anon(user) and not host in ('68.6.35.253', '74.208.44.105'): return apache.HTTP_FORBIDDEN form = util.FieldStorage(req) query = parm(form, 'q') head(req) req.write('') req.write('') req.write('') req.write('') sql = getdb() try: csr = cursor(sql) try: csr.execute(query, locals()) for col in csr.description: req.write('' % (exml(col[0]),)) rows = csr.fetchall() finally: csr.close() finally: putdb(sql) req.write('') for row in rows: req.write('') for value in row: req.write('' % (exml(str(value)),)) req.write('') req.write('
    %s
    %s
    ') req.write('') return apache.OK def handler(req): uri = req.uri if uri.startswith('/_'): uri = uri[2:] slash = uri.find('/') if slash != -1: return back(req, uri[0:slash], uri[slash:]) elif uri == 'query': return query(req) elif uri == 'history': return history(req, None) elif uri == 'search': return search(req) elif uri == 'sitemap': return sitemap(req) elif uri == 'time': return time(req, None) return apache.HTTP_NOT_FOUND else: return puri(req, uri)