This article will describe how to create a trackback handler for use with MVC and the more traditional webforms.
What is a trackback?
A trackback is a way to be notified when a website links to a resource on your own site. Some blogging software supports automatic linking, so if a post on that site links to another, when the post is submitted, it will automatically detect the link and attempt to send a trackback to the original author. If successful, a link is generally created from the original author to the new post, thus building a web of interconnected resources (in theory). You can learn a little more about trackbacks from Wikipedia.
The full trackback specification can be viewed at the SixApart website.
A trackback Handler in C#
Unlike pingbacks (which we'll address in a future article), trackbacks use standard HTTP requests and so are extremely easy to implement.
Available for download at the top of this article is a sample library which you can use to implement your trackbacks.
As a trackback is comprised of several pieces of information which we'll be passing about, we'll start by defining a structure to hold this information.
public struct TrackbackInfo
{
public string BlogName { get; set; }
public string Excerpt { get; set; }
public string Id { get; set; }
public string Title { get; set; }
public Uri Uri { get; set; }
}
The properties of this structure mirror the required information from the trackback specification.
Next, we'll define an enum
for the different result codes you can return. The specification states 0
for success and 1
for error, but I'm uncertain if you can extend this, i.e., is any non-zero classed as an error. We'll play it safe and just use a single error code.
public enum TrackbackErrorCode
{
Success,
Error
}
I'd considered two ways of implementing this, the first being an abstract
class containing methods which must be implemented in order to provide the functionality for saving a trackback into your chosen data source, or using delegates. In order to make it as simple as possible to use, I went with the latter. Therefore, we need two delegates, one which will resolve the "permalink" for the given ID, and another to actually save the trackback.
public delegate Uri GetTrackbackUrlDelegate(TrackbackInfo trackback);
public delegate void SaveTrackbackDelegate(TrackbackInfo trackback);
Implementing the Handler
We've created a static
class named TrackbackHandler
which contains all the functionality we'll need. We expose a single public
method, GetTrackback
, which will return the XML block required to notify the sender of the result of the request.
public static string GetTrackback(NameValueCollection form,
SaveTrackbackDelegate saveTrackbackDelegate, GetTrackbackUrlDelegate getTrackbackUrlDelegate)
{
string url;
if (form == null)
throw new ArgumentNullException("form");
if (saveTrackbackDelegate == null)
throw new ArgumentNullException("saveTrackbackDelegate");
if (getTrackbackUrlDelegate == null)
throw new ArgumentNullException("getTrackbackUrlDelegate");
url = form["url"];
if (!string.IsNullOrEmpty(url) && url.Contains(","))
url = url.Split(',')[0];
return TrackbackHandler.GetTrackback(saveTrackbackDelegate, getTrackbackUrlDelegate,
form["id"], url, form["title"], form["excerpt"], form["blog_name"]);
}
This function accepts the following arguments:
- A
NameValueCollection
holding the submitted trackback data - supporting both the MVC FormCollection
or Request.Form
for ASP.NET.
- An implementation of the
SaveTrackbackDelegate
delgate for saving the trackback to your chosen data store.
- An implementation of the
GetTrackbackUrlDelegate
for resolving a permalink URL of the given ID.
Assuming none of these are null
, the method then calls a private
overload, explicitly specifying the individual items of data.
private static string GetTrackback(SaveTrackbackDelegate saveTrackbackDelegate,
GetTrackbackUrlDelegate getTrackbackUrlDelegate, string id, string url,
string title, string excerpt, string blogName)
{
string result;
try
{
HttpRequest request;
request = HttpContext.Current.Request;
if (string.IsNullOrEmpty(id))
result = GetTrackbackResponse(TrackbackErrorCode.Error, "The entry ID is missing");
else if (request.HttpMethod != "POST")
result = GetTrackbackResponse(TrackbackErrorCode.Error, "An invalid request was made.");
else if (string.IsNullOrEmpty(url))
result = TrackbackHandler.GetTrackbackResponse
(TrackbackErrorCode.Error, "Trackback URI not specified.");
First, we validate that the request is being made via a POST
and not any other HTTP request, and that both the entry ID and the URL of the sender are specified.
else
{
TrackbackInfo trackbackInfo;
string trackbackTitle;
Uri targetUri;
trackbackInfo = new TrackbackInfo()
{
Id = id,
Title = title,
BlogName = blogName,
Excerpt = excerpt,
Uri = new Uri(url)
};
targetUri = getTrackbackUrlDelegate.Invoke(trackbackInfo);
If everything is fine, we then construct our TrackbackInfo
object for passing to our delegates, and then try and get the permalink for the trackback ID.
if (targetUri == null)
result = GetTrackbackResponse
(TrackbackErrorCode.Error, "The entry ID could not be matched.");
else if (!TrackbackHandler.CheckSourceLinkExists
(targetUri, trackbackInfo.Uri, out trackbackTitle))
result = GetTrackbackResponse(TrackbackErrorCode.Error,
string.Format("Sorry couldn't find a link for \"{0}\"
in \"{1}\"", targetUri.ToString(), trackbackInfo.Uri.ToString()));
If we don't have a URL, we return an error code to the sender.
If we do have a URL another method, CheckSourceLinkExists
is called. This method will download the HTML of the caller and attempt to verify if the senders page does in fact contain a link matching the permalink. If it doesn't, then we'll abort here.
If the method is successful and a link is detected, the method will return the title of the senders HTML page as an out
parameter. This will be used if the trackback information didn't include a blog name (as this is an optional field).
else
{
if (string.IsNullOrEmpty(blogName))
trackbackInfo.BlogName = trackbackTitle;
saveTrackbackDelegate.Invoke(trackbackInfo);
result = TrackbackHandler.GetTrackbackResponse(TrackbackErrorCode.Success, string.Empty);
}
}
}
catch (Exception ex)
{
result = TrackbackHandler.GetTrackbackResponse(TrackbackErrorCode.Error, ex.Message);
}
return result;
}
Finally, if everything went to plan, we save the trackback to our data store, and return a success code. In the event of any part of this process failing, then we return an error result.
Downloading the Senders HTML and Checking if a Link Exists
In this implementation, we won't link to the senders site unless they have already linked to us. We do this by downloading the HTML of the senders site and checking to see if our link is present.
private static bool CheckSourceLinkExists(Uri lookingFor, Uri lookingIn, out string pageTitle)
{
bool result;
pageTitle = null;
try
{
string html;
html = GetPageHtml(lookingIn);
if (string.IsNullOrEmpty(html.Trim()) | html.IndexOf
(lookingFor.ToString(), StringComparison.InvariantCultureIgnoreCase) < 0)
result = false;
else
{
HtmlDocument document;
document = new HtmlDocument();
document.LoadHtml(html);
pageTitle = document.GetDocumentTitle();
result = true;
}
}
catch
{
result = false;
}
return result;
}
private static string GetPageHtml(Uri uri)
{
WebRequest request;
HttpWebResponse response;
string encodingName;
Encoding encoding;
string result;
request = WebRequest.Create(uri);
response = (HttpWebResponse)request.GetResponse();
encodingName = response.ContentEncoding.Trim();
if (string.IsNullOrEmpty(encodingName))
encodingName = "utf-8";
encoding = Encoding.GetEncoding(encodingName);
using (Stream stream = response.GetResponseStream())
{
using (StreamReader reader = new StreamReader(stream, encoding))
result = reader.ReadToEnd();
}
return result;
}
private static string GetDocumentTitle(this HtmlDocument document)
{
HtmlNode titleNode;
string title;
titleNode = document.DocumentNode.SelectSingleNode("//head/title");
if (titleNode != null)
title = titleNode.InnerText;
else
title = string.Empty;
title = title.Replace("\n", "");
title = title.Replace("\r", "");
while (title.Contains(" "))
title = title.Replace(" ", " ");
return title.Trim();
}
The function GetDocumentTitle
uses the Html Agility Pack to parse the HTML looking for the title tag. As the CheckSourceLinkExists
function is only checking to see if the link exists somewhere inside the HTML, you may wish to update this to ensure that the link is actually within an anchor tag - the Html Agility Pack makes this extremely easy.
Returning a Response
In several places, the GetTrackback
method calls GetTrackbackResponse
. This helper function returns a block of XML which describes the result of the operation.
private static string GetTrackbackResponse(TrackbackErrorCode errorCode, string errorText)
{
StringBuilder builder;
builder = new StringBuilder();
using (StringWriter writer = new StringWriter(builder))
{
XmlWriterSettings settings;
XmlWriter xmlWriter;
settings = new XmlWriterSettings();
settings.Indent = true;
settings.Encoding = Encoding.UTF8;
xmlWriter = XmlWriter.Create(writer, settings);
xmlWriter.WriteStartDocument(true);
xmlWriter.WriteStartElement("response");
xmlWriter.WriteElementString("response", ((int)errorCode).ToString());
if (!string.IsNullOrEmpty(errorText))
xmlWriter.WriteElementString("message", errorText);
xmlWriter.WriteEndElement();
xmlWriter.WriteEndDocument();
xmlWriter.Close();
}
return builder.ToString();
}
Implementing an MVC Action for Handling trackbacks
In order to use the handler from MVC, define a new action which returns a ContentResult
. It should only be callable from a POST
, and ideally it shouldn't validate input. Even if you don't want HTML present in your trackbacks, you should strip any HTML yourself - if you have ASP.NET validation enabled and an attempt is made to post data containing HTML, then ASP.NET will return the yellow screen of death HTML to the sender, not the nice block of XML it was expecting.
Simply return a new ContentResult
containing the result of the GetTrackback
method and a mime type of text/xml, as shown below:
[AcceptVerbs(HttpVerbs.Post)]
[ValidateInput(false)]
public ContentResult Trackback(FormCollection form)
{
string xml;
if (string.IsNullOrEmpty(form["id"]))
form.Add("id", Request.QueryString["id"]);
xml = TrackbackHandler.GetTrackback(form, this.SaveTrackbackComment, this.GetArticleUrl);
return this.Content(xml, "text/xml");
}
In this case, I'm also checking the query string for the ID of the article to link to as we use a single trackback action to handle all resources. If your trackback submission URL is unique for resource supporting trackbacks, then you wouldn't need to do this.
The implementations of your two delegates will vary depending on how your own website is structured and how it stores data. As an example, I have included the ones used here at Cyotek.com (Entity Framework on SQL Server 2005 using a repository pattern):
private Uri GetArticleUrl(TrackbackInfo trackback)
{
Article article;
int articleId;
Uri result;
Int32.TryParse(trackback.Id, out articleId);
article = this.ArticleService.GetItem(articleId);
if (article != null)
result = new Uri(Url.Action("display", "article", new { id = article.Name }, "http"));
else
result = null;
return result;
}
private void SaveTrackbackComment(TrackbackInfo trackback)
{
try
{
Comment comment;
Article article;
StringBuilder body;
string blogName;
article = this.ArticleService.GetItem(Convert.ToInt32(trackback.Id));
blogName = !string.IsNullOrEmpty(trackback.BlogName) ?
trackback.BlogName : trackback.Uri.AbsolutePath;
body = new StringBuilder();
body.AppendFormat("[b]{0}[/b]\n", trackback.Title);
body.Append(trackback.Excerpt);
body.AppendFormat(" - Trackback from {0}", blogName);
comment = new Comment();
comment.Article = article;
comment.AuthorName = blogName;
comment.AuthorUrl = trackback.Uri.ToString();
comment.DateCreated = DateTime.Now;
comment.Body = body.ToString();
comment.IsPublished = true;
comment.AuthorEmail = string.Empty;
comment.AuthorUserName = null;
this.CommentService.CreateItem(comment);
ModelHelpers.SendCommentEmail(this, article, comment, this.Url);
}
catch (System.Exception ex)
{
CyotekApplication.LogException(ex);
throw;
}
}
Implementing an ASP.NET Webforms trackback Handler
Using this library from ASP.NET webforms is almost as straightforward. You could, as in the example below, create a normal page containing no HTML such as trackback.aspx which will omit the XML when called.
Ideally however, you would probably want to implement this as a HTTP Handler, although this is beyond the scope of this article.
using System;
using System.Text;
using Cyotek.Web.Trackback;
public partial class TrackbackHandlerPage : System.Web.UI.Page
{
protected override void OnInit(EventArgs e)
{
base.OnInit(e);
Response.ContentEncoding = Encoding.UTF8;
Response.ContentType = "text/xml";
Response.Clear();
Response.Write(TrackbackHandler.GetTrackback
(Request.Form, this.SaveTrackback, this.GetTrackbackUrl));
}
private Uri GetTrackbackUrl(TrackbackInfo trackbackInfo)
{
throw new NotImplementedException();
}
private void SaveTrackback(TrackbackInfo trackbackInfo)
{
throw new NotImplementedException();
}
}
Providing the trackback URL
Of course, having a trackback handler is of no use if third party sites can't find it! For sites to discover your trackback URLs, you need to embed a block of HTML inside your blog articles containing a link to your trackback handler. This URL should be unique for each article. For cyotek.com, we append the ID of the article as part of the query string of the URL, then extract this in the controller action, but this isn't the only way to do it - choose whatever suits the needs of your site.
The following shows the auto discovery information for this URL:
<rdf:RDF xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:trackback="http://madskills.com/public/xml/rss/module/trackback/">
<rdf:description
rdf:about="http://cyotek.com/article/display/creating-a-trackback-handler-using-csharp"
dc:identifier="http://cyotek.com/article/display/creating-a-trackback-handler-using-csharp"
dc:title="Creating a trackback handler using C#"
trackback:ping="http://cyotek.com/trackback?id=21" />
It includes the trackback URL (with article ID 21) and the title of the article, plus the permalink.
Next Steps
Cyotek.com doesn't get a huge amount of traffic, and so this library has not been extensively tested. It has worked so far, but I can't guarantee it to be bug free!
Possible enhancements would be to add some form of blacklisting, so if you were getting spam requests, you could more easily disable these. Also the link checking could be made more robust by ensuring it's within a valid anchor, although there's only so much you can do.
I hope you find this library useful, the download link is above. As mentioned, this library uses the Html Agility Pack for parsing HTML, however you can replace this if required with your own custom solution.