jQuery AJAX – redirect on unathorised request

Problem

In default ASP.NET MVC setup, when you send a AJAX request to the MVC Action which returns JSON or simple type value (boolean / string) and the request is not authenticated (user has just logged out or authentication cookie has expired) your jQuery success callback will be fired with a login page HTML content as data. That happens because default AuthorizeAttribute redirects unauthenticated requests to he login page, and jQuery follows this redirect and does not provide a simple way of telling what just happened nor why the redirect was performed.

Solution

To overcome this inconvenience you can roll custom AuthorizeAttribute which will redirect standard HTML requests (caused by forms posting and browser url change) to the login change but return an error code (which can be detected on the jQuery script side) for AJAX request.

public class AJAXAwareAuthorizeAttribute : AuthorizeAttribute
    {
        protected override void HandleUnauthorizedRequest(AuthorizationContext filterContext)
        {
            if (filterContext.RequestContext.HttpContext.Request.IsAjaxRequest())
                filterContext.Result = new HttpStatusCodeResult((int)System.Net.HttpStatusCode.Forbidden);
            else
                base.HandleUnauthorizedRequest(filterContext);
        }
    }

 

Then you can add a global AJAX handler to check for the 403 (Forbidden) code on each AJAX request and redirect/show error/do anything else when this code is returned from the controller.

$(document).ajaxError(function (event, xhr, settings, error) {
    //when there is an AJAX request and the user is not authenticated -> redirect to the login page
    if (xhr.status == 403) { // 403 - Forbidden
        window.location = '/login';
    }
});
 Now your success callback will not fire for unauthenticated requests and the error callback will be triggered instead
Advertisements

SharePoint 2010 + AJAX + Partial rendering

In one of our team’s recent SharePoint projects I was asked to create friendly-looking
panel which would aggregate system notifications sent to currently logged user plus display some graphical representation
of currently running processes started by this user.

Few problems occured:
– there were large volumes of data to display (hundreds or thounsands of orders)
– clients were using very slow PCs
– the system was implemented in SharePoint 2010 (no lightweight ASP.NET MVC-like partial render options)

It would be terribly time consuming to render HTML for every single order (or even just one page of results)
because of DB queries required to generate view and ammount of HTML code which would be sent to users’ browsers.

So here is the solution. It’s actually an attempt to use ASP.NET MVC -like partial rendering + AJAX in SharePoint 2010 environment.

Part 1. HTML placeholder

<a class="header ajax_enabled corner"href="#"  order_id="<%= Model.OrderId %>" is_loaded="0">
    <%= Model.TitleDescription %><!-- when user clicks this element for the first time, AJAX request will download widget layout from server and place it in 'ajax_process_placeholder' div -->
</a>

<divclass="ajax_process_placeholder">        
    <!-- here goes AJAX-retrieved content -->
</div>

Par2. JavaScript code:

function ConfigureAjaxProcessPlaceholder() {
    //script that initializes AJAX behavior of UI widget

    //when user clicks widget header...
    $("div.panel a.header.ajax_enabled").click(function () {
        var parent = $(this);
        var orderId = parent.attr("order_id"); //get order identifier
        var isLoaded = parent.attr("is_loaded"); //check if this element was already loaded

        var placeholder = parent.parent().next("div.ajax_process_placeholder");

        //if this element has not yet been loaded 
        if (isLoaded != "1") {
            //shows some nice 'pleas wait.. loading' banner - hardcoded url is ugly but it's here just to show some concept
            placeholder.append("<img alt='' src='Style%20Library/images/progressbar.gif' />");

            //loads widgets HTML from server (prepared by HTTP Handler)
            $.ajax({
                type: "GET",
                url: "/_layouts/My.Site.Content/StartPanelHandler.ashx", //hardcoded url is bad practice but it's just an example 
                data: { OrderId: orderId, TimeStamp: new Date().getTime() },
                contentType: "application/x-www-form-urlencoded; charset=UTF-8",
                dataType: "html",
                success: function (msg) {

                    //do some jQuery magic: put widget to placeholder
                    var tmp = $("<div/>").html(msg);
                    placeholder.html("");
                    placeholder.append(tmp.find("div.cycle"));

                    //mark placeholder as already loaded. Next time it will be clicked, no AJAX request will be performed. Instead, hidden HTML content will be presented
                    parent.attr("is_loaded", "1");

                    //do animation & show element
                }
            });
        } else {
            //do animation & show element
        }
        return false;
    });
}

Part 3. Handler declaration ASHX file (which goes to SharePoint site “layouts” folder).

<%@ WebHandler Language="C#"  Class="My.Site.Content.ControlTemplates.Widgets.StartPanelHandler, My.Site.Content, Version=1.0.0.0, Culture=neutral, PublicKeyToken=1010101010100101" %>

Part 4. Handler implementation. It does work of handling AJAX request and returning widget HTML.

using System;
using System.Web;
using My.Site.AccessData;
using System.Web.SessionState;
using System.Data.Linq;
using System.Web.UI;
using System.IO;

namespace My.Site.Content.ControlTemplates.Widgets
{
    public class StartPanelHandler : IHttpHandler, IRequiresSessionState
    {
        #region IHttpHandler Members

        public bool IsReusable
        {
            get { return true; }
        }

        //handle AJAX request
        public void ProcessRequest(HttpContext context)
        {
            if (context.Request["OrderId"] == null)
            {
                ReturnObjectNotFound(context);
                return;
            }

            int orderId = Convert.ToInt32(context.Request["OrderId"]);

            using (var ctx = new DataContext(Settings.ConnectionString))
            {
                var feed = new OrdersFeedAdapter(ctx);

                var statusAdapter = new OrderProgressAdapter(feed);
                var statusModel = statusAdapter.GetModel(orderId, false);

                if (statusModel == null)
                    ReturnObjectNotFound(context);
                else
                    ReturnObjectView(statusModel, context);
            }
        }
        #endregion

        //ASP.NET MVC-like partial view render
        private void ReturnObjectView(OrderProgressModel statusModel, HttpContext context)
        {
            context.Response.ContentType = "text/html; charset=utf-8";

            var page = new Page();
            var ctrl = page.LoadControl("~/_CONTROLTEMPLATES/My.Site.Content/Widgets/OrderProgressWidget.ascx") as OrderProgressWidget;
            ctrl.Model = statusModel;
            page.Controls.Add(ctrl);

            using (var stream = new StreamWriter(context.Response.OutputStream))
            {
                HttpContext.Current.Server.Execute(page, stream, false);
                stream.Flush();
                stream.Close();

                context.Response.End();
            }
        }

        private void ReturnObjectNotFound(HttpContext context)
        {
            context.Response.ContentType = "text/html; charset=utf-8";
            context.Response.Write("<span class='error'>Object not found or access is denied</span>");
        }
    }
}

So, what happens here is:

When user clicks widget placeholder for the first time, AJAX request is fired. This request is handled by ASP.NET HTTP Handler which prepares HTML content of widget (depending on Order ID parameter). TimeStamp parameter is added to the request in order to avoid the request result being cached by IE browsers. Then HTML content of widget is returned and put into the corresponding placeholder. Then the placeholder is marked as “loaded” so when user clicks it again no AJAX request will be fired because HTML code is already in there and all we need to do is to show this hidden code.

That did the trick – performance is not a problem, customer is satisfied 😉