torsdag 9 september 2010

The ListItemProperty Web Control

The issue: in the forms.aspx page, the <SharePoint:ListItemProperty> should render the Title field in the SPListItem. Sure it works. Almost as you would expect. My title is 255 characters but the MaxLength property is set to 40 in the forms.aspx. Nice. But no... All characters are rendered. Why? Search on google: nothing. Reflector (thanks Lutz Roeder for saving my life so many times!): yes!

Looking again at the web control you might notice that the Property property is not set to "Title" so you can assume that it is the default property to render, and yes it is. But when looking at the code you can see that if the property isn't set to anything, the control renders simply SPContext.Current.ListItemDisplayName. It doesn't truncate the output to match the MaxLength property. But when the Property property is set in the web control, the Render method uses the SPWeb.Request.RenderColumn method which takes all properties, including MaxLength.

So since it actually affects all lists you could actually edit the actual form.aspx file in the Pages folder in the hive. I don't since it's a out-of-the-box file, it's simple enough to use a copy.

onsdag 8 september 2010

Authorized Images in an FBA Login Page

My first post. I intend to use this blog mostly just to remind myself about useful stuff I've done, but if it can help anybody else I'll be happy.
A customer wants to a have a login page but still using authentication from their AD. First I thought, not possible since NTLM just allows the normal windows login popup. A former colleague said that I maybe could consider making it a forms based authentication application using the ActiveDirectoryMembershipProvider together with SSL. It was ok for the customer but maybe that will be another post.
The Big problem was to design the login page. With their current oracle-based solution it would take up to two weeks to get a change in the login page. Requests to the hosting company, decisions, meetings etc, and ok they accept to change the . to a , .
Now a user can edit the the login page using an announcement item in a list. The login page runs a code with elevated privileges to get the latest announcement and places the Title and Body fields in the page in the Render method. Nice.
But after a while they noticed that they couldn't use an image in the body. Hmmm... why not? Aha, the image is a link to an attachment in the item. Damn! The login page gets the correct html, but since the user isn't logged in yet, the img-tags can't load the images.
First I thought a solution would be to take a "snap shot" of the body including the images and put it in the LAYOUTS folder and set the location as allowed in web.config. But when I tried to write the stuff to LAYOUTS I noticed that not even the Application Pool account have write access.
Ok, another idea. Replace the url to the image with a url to an aspx-file that takes the image's url in the query string, runs code with elevated privileges to get the image and in the Page.Response writes the image as a byte[].
I still had to set that the aspx-file is allowed non-authenticated access in web.config. But hey, it worked! But just to be careful I made the url in the query string as a Base64Encoded string (of course I could have made a somewhat more safe encoding, but time is money).

So, the solution was:
An aspx-file namned getimage.aspx placed in the LAYOUTS folder containing

<%@ Assembly Name="MyProject, Version=1.0.0.0, Culture=neutral, PublicKeyToken=3334a3f90ce5bdd0" %>
<%@ Page Language="C#" Inherits="MyProject.Code.ApplicationPages.GetImage" %>
 
The class MyProject.Code.ApplicationPages.GetImage
 
public class GetImage : Microsoft.SharePoint.WebControls.UnsecuredLayoutsPageBase

{
protected override void OnLoad(EventArgs e)
{
  if (string.IsNullOrEmpty(Request.QueryString["url"]))
    return;

  SPSecurity.RunWithElevatedPrivileges(() =>
  {
    string siteUrl = string.Format("{0}://{1}", HttpContext.Current.Request.Url.Scheme, HttpContext.Current.Request.Url.Host);
    string url = Base64Decode(Request.QueryString["url"]);
    using (SPSite site = new SPSite(siteUrl))
    {
      using (SPWeb web = site.OpenWeb())
      {
        SPFile file = web.GetFile(url);
        Page.Response.BinaryWrite(file.OpenBinary(SPOpenBinaryOptions.None));
      }
    }
  });
}
private static string Base64Decode(string sBase64String)
{
  byte[] sBase64String_bytes = Convert.FromBase64String(sBase64String);
  return UnicodeEncoding.UTF8.GetString(sBase64String_bytes);
}

And the really tricky part: correcting the urls in the body in the login page. It's a little part in the Render method (The class is called MyProject.Code.ApplicationPages.LoginPage and inherits from Microsoft.SharePoint.ApplicationPages.LoginPage):

protected override void Render(System.Web.UI.HtmlTextWriter writer)
{
  SPSecurity.RunWithElevatedPrivileges(() =>
  {
    string url = string.Format("{0}://{1}", HttpContext.Current.Request.Url.Scheme, HttpContext.Current.Request.Url.Host);
    using (SPSite site = new SPSite(url))
    {
      using (SPWeb web = site.RootWeb)
      {
        var list = web.GetList("/lists/loginmessages");
        var query = new SPQuery();
        query.Query = "<Where><Geq><FieldRef Name=\"Expires\"/><Value Type=\"DateTime\"><Today/></Value></Geq></Where><OrderBy><FieldRef Name=\"Expires\" Ascending=\"FALSE\"/></OrderBy>";
        var items = list.GetItems(query);
        if (items.Count > 0)
        {
          lblTitle.Text = items[0].Title;
          string body = Convert.ToString(items[0][SPBuiltInFieldId.Body]);
          Regex regex = new System.Text.RegularExpressions.Regex("(?<=\\<img.*src=\")((/lists/loginmessages/attachments/\\d*/)[-a-zA-Z0-9@:%_\\+.~#?&//=]+)\\.(jpg|jpeg|gif|png|bmp|tiff|tga|svg)", RegexOptions.IgnoreCase);
          body = regex.Replace(body, "/_layouts/getimage.aspx?url=$&");
          regex = new Regex("(?<=\\<img.*src=\"/_layouts/getimage\\.aspx\\?url=)((/lists/loginmessages/attachments/\\d*/)[-a-zA-Z0-9@:%_\\+.~#?&//=]+)\\.(jpg|jpeg|gif|png|bmp|tiff|tga|svg)", RegexOptions.IgnoreCase);
          body = regex.Replace(body, new MatchEvaluator(LoginPage.ReplaceEncode));
          litBody.Text = body;
        }
      }
    }
  });
}

and the methods to do the encoding in the login page:

private static string ReplaceEncode(Match match)
{
  return match.Result(Base64Encode(match.Value));
}
private static string Base64Encode(string sString)
{
  byte[] sString_bytes = System.Text.UnicodeEncoding.UTF8.GetBytes(sString);
  return Convert.ToBase64String(sString_bytes);
}