# Pingback plugin for vellum
# Implements the Pingback spec: http://www.hixie.ch/specs/pingback/pingback
# Requires xmlrpclib:
#   Included with Python 2.2+
#   Otherwise get from http://www.pythonware.com/products/xmlrpc/
#     and place in your plugins directory

import vellum.hooks,vellum.Entry,vellum.functions,vellum.config
import re,sys,os,time,bsddb,pickle
import xmlrpclib,urllib2,urlparse
from vellum.vellumObject import vellumObject

__pluginname__ = "Pingback"
__description__ = """Implements the Pingback spec, available at
<a href="http://www.hixie.ch/specs/pingback/pingback">http://www.hixie.ch/specs/pingback/pingback</a>."""


# --------------------------------------------------------------
#                   Pingback server
# --------------------------------------------------------------

class Pingback(vellumObject):
    id = None
    url = ''
    title = ''
    excerpt = ''
    entryid = None
    created = None

    def save(self):
        self.setMyID()
        if not self.created: self.created = time.time()
        vellumObject.save(self)

dbdir = vellum.config.get("DatabaseDir")
if not dbdir:
    raise "There is no DatabaseDir listed in the config file"

Pingback.db = bsddb.hashopen(os.path.join(dbdir,"pingbacks"),"c")

def all_pingbacks():
    return map(get_pingback,Pingback.db.keys())

def get_pingback(id):
    "Gets one individual pingback by ID"
    sid = str(id)
    if Pingback.db.has_key(sid):
        k,v = Pingback.db.set_location(sid)
        return pickle.loads(v)
    else:
        return None

def get_pingbacks(entry):
    "Gets all pingbacks for the passed entry"
    ep = filter(lambda x,i=entry.id:str(x.entryid)==str(i),all_pingbacks())
    ep.sort(lambda x,y:cmp(y.created,x.created))
    return ep

# Add the pingbacks() method to Entry
vellum.Entry.Entry.pingbacks = get_pingbacks

# Add an xmlrpc server to functions
def pingbackXMLRPCserver(form):
    # We should have been called without a cgi object being invoked,
    # which means that sys.stdin has not been consumed
    rpc = sys.stdin.read()
    # Get the passed data
    # Give it to xmlrpclib
    data,method = xmlrpclib.loads(rpc)

    # dispatch appropriately
    if method == "pingback.ping":
        receive_ping(data)
    elif method == "system.getCapabilities":
        get_capabilities()
    elif method == "system.listMethods":
        list_methods()
    else:
        f = xmlrpclib.Fault(-32601,"Method '%s' not found" % method)
        print xmlrpclib.dumps(f)

    # And terminate so we don't print the vellum headers etc
    sys.exit(0)

vellum.functions.pingback = pingbackXMLRPCserver
# Don't need to be authenticated to call the pingback function
vellum.functions.pingback.noAuth = 1
# and it wants to parse stdin itself
vellum.functions.pingback.noStdinParsing = 1

def receive_ping(data):
    # Actually receive a ping
    if type(data) <> type(()):
        f = xmlrpclib.Fault(-32602,"You must pass two parameters, the linking URI and the linked-to URI")
        print xmlrpclib.dumps(f)
        return
    if len(data) <> 2:
        f = xmlrpclib.Fault(-32602,"You must pass two parameters, the linking URI and the linked-to URI")
        print xmlrpclib.dumps(f)
        return
    linkFrom,linkTo = data

    # Confirm that linkFrom contains a link to linkTo
    req = urllib2.Request(linkFrom)
    try:
        ufp = urllib2.urlopen(req)
    except:
        f = xmlrpclib.Fault(0,"I could not fetch %s for checking" % linkFrom)
        print xmlrpclib.dumps(f)
        return
    content = ufp.read()
    if content.find(linkTo) == -1:
        f = xmlrpclib.Fault(17,"%s does not link to %s" % (linkFrom,linkTo))
        print xmlrpclib.dumps(f)
        return

    # Work out which entry linkTo points to
    try:
        import vellum.Blog
        matchingEntry = None
        for blog in vellum.Blog.all():
            for entry in blog.entries():
                if entry.permalink == linkTo:
                    matchingEntry = entry
        if not matchingEntry:
            f = xmlrpclib.Fault(33,"%s is not a pingbackable resource" % linkTo)
            print xmlrpclib.dumps(f)
            return
    except:
        import StringIO,traceback
        catcherr = StringIO.StringIO()
        traceback.print_exc(file=catcherr)
        errtext = catcherr.getvalue()
        f = xmlrpclib.Fault(0,"Something bad happened while finding the local entry (%s)" % errtext)
        print xmlrpclib.dumps(f)
        return

    # Check that we haven't received this pingback before
    try:
        for pb in matchingEntry.pingbacks():
            if pb.url == linkFrom:
                f = xmlrpclib.Fault(48,"This pingback has already been registered")
                print xmlrpclib.dumps(f)
                return
    except:
        import StringIO,traceback
        catcherr = StringIO.StringIO()
        traceback.print_exc(file=catcherr)
        errtext = catcherr.getvalue()
        f = xmlrpclib.Fault(0,"Something bad happened while confirming that we haven't already seen this pingback (%s)" % errtext)
        print xmlrpclib.dumps(f)
        return
            
    # Now that we have an entry, construct a pingback
    try:
        pb = Pingback()
        pb.url = linkFrom
        pb.entryid = matchingEntry.id
        title_re = re.compile('<title>(?P<title>[^<]+)</title>',re.I | re.S)
        titles = title_re.findall(content)
        if len(titles) == 0:
            pb.title = linkFrom
        else:
            pb.title = titles[0]
    except:
        f = xmlrpclib.Fault(0,"Something bad happened while constructing a pingback object")
        print xmlrpclib.dumps(f)
        return

    # and add the pingback
    try:
        pb.save()
        matchingEntry.save()
    except:
        import StringIO,traceback
        catcherr = StringIO.StringIO()
        traceback.print_exc(file=catcherr)
        errtext = catcherr.getvalue()
        f = xmlrpclib.Fault(0,"Something bad happened while saving the pingback (%s)" % errtext)
        print xmlrpclib.dumps(f)
        return

    response = "You pinged %s from %s. Thanks for the heads-up!" % (linkTo,linkFrom)
    print xmlrpclib.dumps((response,))
    return

def get_capabilities():
    # We have to implement this because we use extended error codes, as stated
    # by http://xmlrpc-epi.sourceforge.net/specs/rfc.fault_codes.php
    response = {
                    "faults_interop": {
                        "specURL": 'http://xmlrpc-epi.sourceforge.net/specs/rfc.fault_codes.php',
                        "specVersion": 20010516
                    }
               }
    print xmlrpclib.dumps((response,))
    return

def list_methods():
    response = ['pingback.ping','system.getCapabilities','system.listMethods']
    print xmlrpclib.dumps((response,))
    return

# --------------------------------------------------------------
#                   Pingback client
# --------------------------------------------------------------

def pingback_send_pings(entry):
    # REs we need
    pingbackElement_re = re.compile('<link rel="pingback" href="([^"]+)">',re.I | re.S)
    link_re = re.compile('<a[^>]*href="(?P<href>[^"]+)"[^>]*>',re.I | re.S)

    text = entry.body
    if entry.extended: text += entry.extended

    # Parse all links out of the text
    links = link_re.findall(text)
    for link in links:
        try:
            absoluteLink = urlparse.urljoin(entry.permalink,link)
            req = urllib2.Request(absoluteLink,None,{"Referer":entry.permalink})
            ufp = urllib2.urlopen(req)
            pingbackServer = None
            # Look for an X-Pingback header (sect 2.1)
            hdrs = ufp.info().headers
            for hdr in hdrs:
                hl = hdr.split(':',1)
                if len(hl) == 2:
                    nm,vl = hl
                    if nm.lower() == 'x-pingback':
                        pingbackServer = vl.strip()
            if not pingbackServer:
                # Parse the body
                body = ufp.read()
                pbElements = pingbackElement_re.findall(body)
                if len(pbElements) > 0:
                    pingbackServer = pbElements[0]

            if pingbackServer:
                # Actually ping the smegger
                svr = xmlrpclib.Server(pingbackServer)
                svr.pingback.ping(entry.permalink,absoluteLink)
                # And ignore the result, heh heh heh

            ufp.close()

        except:
            # Like the raven said, carrion regardless, and
            # don't throw an error for the user to see
            pass

# Run the send_pings function after rebuilding an entry

vellum.hooks.register_hook("entry-rebuild-post",pingback_send_pings)

