22 Sep 2010

Creating a trackback handler using C#

Cyotek.com runs on its own custom CMS/blog engine developed in ASP.NET MVC 1.0, which has a number of advantages and disadvantages. One of these disadvantages is no automatic support for some common blog features such as trackbacks and pingbacks.

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 Wikiepedia.

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 end 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, ie is any non-zero is 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 a simple as possible to use, I've 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 choosen 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)
  {
    //handle the error.
    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.

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;

  // get the ID of the article to link to from the URL query string
  if (string.IsNullOrEmpty(form["id"]))
    form.Add("id", Request.QueryString["id"]);

  // get the response from the trackback handler
  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 URL's, 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:

 
 
 

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 ensure its within a valid anchor, although there's only so much you can do.

I hope you find this library useful, the download link is below. As mentioned, this library uses the Html Agility Pack for parsing HTML, however you can replace this if required with your own custom solution.

Downloads

cyotek.web.trackback.zip

Sample project showing for implementing a trackback handler in C# for use with either ASP.NET or MVC.

22 September 2010 6.50 KB

Comments

  • # DotNetKicks.com
    22/09/2010 20:03

    Creating a trackback handler using C#
    You've been kicked (a good thing) - Trackback from DotNetKicks.com - Trackback from DotNetKicks.com

  • # DotNetShoutout
    22/09/2010 21:17

    Creating a trackback handler using C#
    Thank you for submitting this cool story - Trackback from DotNetShoutout - Trackback from DotNetShoutout

  • Gravatar
    # Agafonov
    27/09/2010 11:41

    Nice post thanks for sharing...

Leave a Comment

While we appretiate comments from our users, please follow our posting guidelines. Have you tried the Cyotek Forums for support from Cyotek and the community?