Solving the "ugly" URL issue for Mac clients – Last Updated: June 22nd, 2006

I have already posted two articles about how to solve the “ugly” URL problem. In the last article I discussed a solution based on a custom http module which does all the necessary checks and adjustments.

But one issue is still unsolved: Internet Explorer on Mac has an ugly bug then whenever the “action” property of a form tag is modified using Javascript a click to all items – even a label – will cause a postback to the server.

To solve this problem Microsoft has posted a fix for MCMS which will avoid this. But my code in the solution referenced above does exactly the same. So using this code in an environment will produce the error again. To avoid this problem it is necessary that the original action property of the template is modified on the server side rather than on the client side.

I looked through lots of documentation but did not find a solution for this dilemma which works in all situations – even if it is necessary to change the action property to point to a location in a different web application.

So finally I decided to implement an http stream filter to solve this problem. The stream filter needs to intercept the http response stream just before it is send to the client and has to modify the action property of the form tag. As this is not a very elegant solution I implemented the filter in a way hat it will only be active if the client requesting is a Mac client.

Below is the code for the relevant stream filter most of the code is only implementing mandatory methods and properties. The real work is done in the Write method:

public class FormTagFilter : Stream
{
    private Encoding enc = null
;
   
private bool
closed;
    Stream BaseStream;

    public FormTagFilter(Stream baseStream, Encoding encoding)
    {
       
BaseStream = baseStream;
        closed =
false
;
        enc = encoding;
    }

    public override void Write(byte[] buffer, int offset, int count) 
    {
       
if(Closed) throw new
ObjectDisposedException(“FormTagFilter”);
       
string
content = enc.GetString(buffer, offset, count);        // corrected Aug 27th, 2004
        byte[] newBuffer = enc.GetBytes(content);
       
int
pos = content.IndexOf(“<form “);´
       
while
(pos >= 0)
        {
            pos = content.IndexOf(“action=”,pos);
           
if
(pos >= 0)´
            {
                pos += “action=\””.Length;
               
int
endpos = content.IndexOf(“\””,pos);
               
if
(endpos > 0)
                {
                    string actionString = content.Substring(pos,endpos-pos);
                    if (actionString.IndexOf(“NRMODE”) > 0)
                    {
                       
string
head = content.Substring(0,pos);
                       
string
tail = content.Substring(endpos);
                        content = head+CmsHttpContext.Current.ChannelItem.UrlModePublished+tail;
                        newBuffer = enc.GetBytes(content);       // corrected Aug 27th, 2004
                        break;
                    }
                }
            }
            pos = content.IndexOf(“<form”,pos);
        }
        count = newBuffer.Length;                                    // corrected Aug 27th, 2004
        BaseStream.Write(newBuffer, 0, count);                  // corrected Aug 27th, 2004
    }

    public override bool CanRead
    { 
       
get { return false
; }
    }

    public override bool CanWrite 
    {
       
get { return
!closed; }
    }

    public override bool CanSeek 
    {
       
get { return false
; }
    }

 &
nbsp; 
public override void
Close() 
    {
        closed =
true
;
        BaseStream.Close();
    }

    protected bool Closed 
    {
       
get { return
closed; }
    }

    public override void Flush() 
    {
        BaseStream.Flush();
    }

    public override int Read(byte[] buffer, int offset, int count) 
    {
       
throw new
NotSupportedException();
    }

    public override long Length 
    {
       
get { throw new
NotSupportedException(); }
    }

    public override long Seek(long offset, SeekOrigin origin) 
    {
       
throw new
NotSupportedException();
    }

    public override void SetLength(long value
    {
       
throw new
NotSupportedException();
    }

    public override long Position 
    {
       
get { throw new
NotSupportedException(); }
       
set { throw new
NotSupportedException(); }
    }
}

To integrate this filter into the response stream I integrated the following code in the OnPreRequestHandlerExecute handler:

if (thisPosting != null && currentMode == PublishingMode.Published)
{
   
// register http filter for Mac browsers only
   
// to avoid performance impact for non-Mac users this code is only activated for Mac clients
    // if (ctx.Request.UserAgent.IndexOf(“Win”) > 0)
   
if
(ctx.Request.UserAgent.IndexOf(“Mac_PowerPC”) > 0)
    {
        FormTagFilter ftf =
new
FormTagFilter(ctx.Response.Filter, ctx.Response.ContentEncoding);
       
ctx.Response.Filter = ftf;
    }
    …
}

To also allow usage of this filter with forms authentication and still handle expired cookies correct you will have to encapsulate the content of the OnPreRequestHandlerExecute routine in a try-catch block as in this situation accessing the CmsHttpContext.Current will give you an access denied exception.

public void OnPreRequestHandlerExecute(object sender, EventArgs e) 
{
    HttpContext ctx = ((HttpApplication)sender).Context;
    IHttpHandler handler = ctx.Handler;

    try
    {
        …
    }
    catch

    {
        // this will happen if the request is in the middle of an expired forms authentication login
        // we just ignore this.
    }
}

Finally I modified the OnInit event to ensure that the __CMS_Page script block is only injected if the browser is not an Mac IE client.

if (currentPage.Request.UserAgent.IndexOf(“Mac_PowerPC”) > 0)
{
    currentPage.RegisterClientScriptBlock(“__CMS_Page”,””);
    currentPage.RegisterStartupScript(“ResetFormActionScript”,””); // required for SP2
}
else
{
    currentPage.RegisterClientScriptBlock(“__CMS_Page”,
// now lets register our script with the nice URL
        “<script language=\”javascript\” type=\”text/javascript\”>\n”+
        “<!–\n”+
        ” var __CMS_PostbackForm = document.”+ctrl.ID+”;\n”+
        ” var __CMS_CurrentUrl = \””+CmsHttpContext.Current.ChannelItem.Url+”\”;\n”+
        ” __CMS_PostbackForm.action = __CMS_CurrentUrl;\n”+
        “// –>\n”+
        “</script>\n”);
        break
;
}

By registering a blank script block for the __CMS_Page script block it is now ensure that the postback issue is also resolved without the hotfix mentioned above.

To implement this solution either modify the code listed here or wait a couple of days till the complete code shows up on GotDotNet. It will take a few days to get the update approved. Just check it out next week.

14 Comments


  1. Hi Kiliman,

    I have looked into Jesse’s approach now after you pointed me to it and both approachs are pretty similiar: both modify the output stream being sent to the client but on different levels.

    Jesse create a new derived page class and adds overloaded methods to this class to do the job while I do it on a lower level by modifying the output stream just before it is being sent to the client.

    I’m not sure why you think that Jesses approach is simpler. Much more different actions have to be modified and in addition it is necessary to ensure that all affected pages are derived from the new page object.

    My approach on the other hand only registers a new filter with the http response stream which works without any need to do modifications to the pages.

    So my solution is completly transparent to the application and installation is pretty simple by adding the http module to the web.config.

    No further modification is necessary!

    Cheers,

    Stefan.

    Reply

  2. Short note:

    Just fixed a small bug. The "count" variable was not adjusted after encoding the stream. The code above should now work in all situations.

    Cheers,

    Stefan.

    Reply

  3. A simpler method if you don’t care about view state data:

    In the .aspx file write a comment open tag just before the form tag: <% Response.Write("<!– "); %>

    And this just after the form tag:

    <% Response.Write(formString); %>

    And define formString in the page class and set it in Page_Load to the form tag you want with a comment close tag before it. You must put the entire tag in the string: formString=" –><form name=__ id=__ method=__ action=__ target=__ … >";

    This will also comment out the hidden view state field, so only use this method if you don’t need it. It’s a bit of an ugly hack, but it works in my situation where I don’t use postbacks.

    – Dave

    Reply

  4. Hi Dave,

    this is a pretty interesting approach!

    Cheers,

    Stefan.

    Reply

  5. Thanks Stefan as always for a great block of code!

    A few issues I found:

    1) The code here and on GotDotNet doesn’t line up:

    a) GotDotNet doesn’t have the fix for count=buffer.Length

    b) GotDotNet seems to have the browser-detect backwards

    2) This doesn’t work for multiple <form> tags (e.g., a search box at the top of the page), since it assumes the first one it finds is the "right" one

    3) The Switch to Live Site redirect doesn’t correctly handle duplicate postings (I believe this has been noted elsewhere), because it goes into an infinite loop

    Some edits I made to account for these (not necessarily perfect, but seem to work) – hope the formatting is readable.

    public override void Write(byte[] buffer, int offset, int count)

    {

    if (Closed)

    throw new ObjectDisposedException("FormTagFilter");

    string content = enc.GetString(buffer);

    int pos = content.IndexOf("<form ");

    while (pos > 0)

    {

    pos = content.IndexOf("action=", pos);

    if (pos >= 0)

    {

    pos += "action="".Length;

    int endpos = content.IndexOf(""", pos);

    if (endpos > 0)

    {

    string actionString = content.Substring(pos, endpos – pos);

    // Only change the action parameter of the CMS <form> tag; leave any others alone

    if (actionString.IndexOf("NRMODE") > 0)

    {

    string head = content.Substring(0, pos);

    string tail = content.Substring(endpos);

    content = head + CmsHttpContext.Current.ChannelItem.UrlModePublished + tail;

    buffer = enc.GetBytes(content);

    count = buffer.Length;

    break;

    }

    }

    }

    pos = content.IndexOf("<form ", pos);

    }

    BaseStream.Write(buffer, offset, count);

    }

    ===

    // Redirect to internal URL of posting. If it is a duplicate posting

    // (i.e., the internal URL is an ugly /NR/exeres URL) allow it to be displayed

    // by flagging as NRUrl=true.

    if (ctx.Request.QueryString["NRORIGINALURL"] != null &&

    ctx.Request.QueryString["NRORIGINALURL"].StartsWith("/NR/exeres") &&

    (ctx.Request.QueryString["NRUrl"] == null || ctx.Request.QueryString["NRUrl"] != "true"))

    {

    if (thisPosting.Url.StartsWith("/NR/exeres"))

    {

    ctx.Response.Redirect (thisPosting.Url + "?NRUrl=true", false);

    }

    else

    {

    ctx.Response.Redirect (thisPosting.Url);

    }

    }

    Reply

  6. Hi Erik,

    ASP.NET does not allow multiple form tags. Search boxes have to be done using user controls in ASP.NET and not using multiple form tags – except if you move the code completly outside the ASP.NET form tags and only use html and javascript and no ASP.NET features to implement this.

    Not very handy.

    The GotDotNet sample update often takes some time to become live. Not sure why the upload is delayed so long. It’s not in my hands.

    Cheers,

    Stefan.

    Reply

  7. Hi Stefan – yes, I should have been clearer on this. We have exactly this scenario – the search <form> tag is a standard HTML form, not runat=server (i.e., <form method="get" action="searchresults.aspx"). It is happenstance that the HTML <form> tag precedes the ASP.NET/CMS <form> tag, but in my case it did require special handling. The code above should work in all cases (whether or not there are extra <form> tags). Thanks! –Erik

    Reply

  8. Hi Erik,

    I have updated the sample on GotDotNet as indicated (will take some time to get live) and also changed the code above to reflect the changes.

    Another change I did is to handle the access denied exception when using forms authentication if the cookie is expired. In this situation it is not possible to access the CmsHttpContext.Current.

    Cheers,

    Stefan.

    Reply

  9. Thanks Stefan – I’ve integrated the new OnInit() method into my code; wouldn’t have caught that myself! 🙂 –Erik

    Reply

  10. There are two different reasons for ugly URL’s on in presentation mode:

    when using Webauthor and…

    Reply

  11. Today a newsgroup post reminded me that I did not yet publish the necessary changes for my Http Module…

    Reply

Leave a Reply to Erik Bailey Cancel reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.