# 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
http://www.hixie.ch/specs/pingback/pingback."""
# --------------------------------------------------------------
# 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('(?P[^<]+)',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('',re.I | re.S)
link_re = re.compile(']*href="(?P[^"]+)"[^>]*>',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)