Entries tagged with 'mvc'

MVC actions, AcceptVerbs, HEAD requests and 404 errors

When running Sitemap Creator on the development version of cyotek.com, we found all links pointing to articles returned a 404 status code when crawling was attempted. But if same URL was copied into a browser, it would load correctly.

This surprised us, as cyotek.com is the main site we test Sitemap Creator and WebCopy on and they've always worked in the past. Next, we tried it directly on cyotek.com, and got the same result. However, this being the release version of the web, we started receiving error emails from the website (these are not sent from the debug builds).

The exception being reported was this:

System.Web.HttpException: A public action method 'display' could not be found on controller 'Cyotek.Web.Controllers.ArticleController'. at System.Web.Mvc.Controller.HandleUnknownAction(String actionName) at System.Web.Mvc.Controller.ExecuteCore() at System.Web.Mvc.ControllerBase.Execute(RequestContext requestContext) at System.Web.Mvc.ControllerBase.System.Web.Mvc.IController.Execute(RequestContext requestContext) at System.Web.Mvc.MvcHandler.ProcessRequest(HttpContextBase httpContext) at System.Web.Mvc.MvcHandler.ProcessRequest(HttpContext httpContext) at System.Web.Mvc.MvcHandler.System.Web.IHttpHandler.ProcessRequest(HttpContext httpContext) at System.Web.HttpApplication.CallHandlerExecutionStep.System.Web.HttpApplication.IExecutionStep.Execute() at System.Web.HttpApplication.ExecuteStep(IExecutionStep step, Boolean& completedSynchronously)

This error message certainly raised eyebrows, as of course, this action does exist.

This is the current definition of the display article action:

[OutputCache(CacheProfile = "Short")]
[AcceptVerbs(HttpVerbs.Get)]
public ActionResult Display(string id, bool? posted)
{
}

As soon as we looked at the code, we realised what had happened. By default both Sitemap Creator and WebCopy make HEAD requests to obtain the headers for a given URL, such as the content type. They use these headers to determine if they should go ahead and download the entire file - Sitemap Creator won't download anything that isn't text/html for example.

And this is the problem - in the last update to cyotek.com, we changed a few site settings to stop the number of error emails occurring due to spammer activity. For some reason the AcceptVerbs attribute was applied to the Display action method at this point. And as it is only set to accept GET, it means our HEAD calls automatically fail.

One changing the attribute, everything started working nicely again.

[AcceptVerbs(HttpVerbs.Get | HttpVerbs.Head)]

For once, a nice and simple mystery to solve, and a nice little tip which will hopefully help anyone else who has a similar issue.

Post a Comment | | Trackback specific URL for this entry

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.

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;

  // 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

    (6.50 KB | 22 September 2010 )

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

3 comments | | Trackback specific URL for this entry

Using XSLT to display an ASP.net sitemap without using tables

The quick and easy way of displaying an ASP.net site map (web.sitemap) in an ASP.net page is to use a TreeView control bound to a SiteMapDataSource component as shown in the following example:

<asp:SiteMapDataSource runat="server" ID="siteMapDataSource" EnableViewState="False"   ShowStartingNode="False" />
<asp:TreeView runat="server" ID="siteMapTreeView" DataSourceID="siteMapDataSource"  EnableClientScript="False" EnableViewState="False" ShowExpandCollapse="False"></asp:TreeView>

Which results in a mass of nested tables, in-line styles, and generally messy mark-up.

With just a little more effort however, you can display the sitemap using a XSLT transform, resulting in slim, clean and configurable mark-up - and not a table to be seen.

This approach can be used with both Web Forms and MVC.

This article assumes you already have a pre-made ASP.net sitemap file.

Defining the XSLT

Add a new XSLT File to your project. In this case, it's named sitemap.xslt.

Next, paste in the mark-up below.

<?xml version="1.0" encoding="utf-8"?>
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform" xmlns:map="http://schemas.microsoft.com/AspNet/SiteMap-File-1.0" exclude-result-prefixes="map">
  <xsl:output method="xml" encoding="utf-8" indent="yes"/>

  <xsl:template name="mapNode" match="map:siteMap">
    <ul>
      <xsl:apply-templates/>
    </ul>
  </xsl:template>

  <xsl:template match="map:siteMapNode">
    <li>
      <a href="http://cyotek.com{substring(@url, 2)}" title="{@description}">
        <xsl:value-of select="@title"/>
      </a>

      <xsl:if test="map:siteMapNode">
        <xsl:call-template name="mapNode"/>
      </xsl:if>

    </li>
  </xsl:template>
  
</xsl:stylesheet>

Note: As generally all URL's in ASP.net site maps start with ~/, the href tag in the above example has been customized to include the domain http://cyotek.com at the start, then use the XSLT substring function to strip the ~/ from the start of the URL. Don't forget to modify the URL to point to your own domain!

Declaratively transforming the document

If you are using Web forms controls, then this may be the more convenient approach for you.

Just add the XML component to your page, and set the DocumentSource property to the name of the sitemap, and the TransformSource property to the name of your XSLT file.

<asp:Xml runat="server" ID="xmlSiteMapViewer" DocumentSource="~/web.sitemap" TransformSource="~/sitemap.xslt" />

Programmatically transforming the document

The ASP.net XML control doesn't need to be inside a server side form tag, so you can use the exact same code above in your MVC views.

However, if you want to do this programmatically, the following code works too.

  var xmlFileName = Server.MapPath("~/web.sitemap");
  var xslFileName=Server.MapPath("~/sitemap.xslt");
  var result =new System.IO.StringWriter();
  var transform = new System.Xml.Xsl.XslCompiledTransform();

  transform.Load(xslFileName);
  transform.Transform(xmlFileName, null, result);

  Response.Write(result.ToString());

The result

The output of the transform will be simple series of nested unordered lists, clean and ready to be styled with CSS. And for little more effort than it took to do the original tree view solution.

With a bit more tweaking you can probably expand this to show only a single branch, useful for navigation within a section of a website, or creating breadcrumb trails.

3 comments | | Trackback specific URL for this entry
  • Page 1 of 1
  • First
  • Previous
  • 1
  • Next
  • Last