Thursday, 20 March 2014

Nicely Branded 40* Page

In my previous post I talked about how to create a http handler to redirect your 401 error to a custom html error page. which is still kind of limited Ideally we would want a branded error page that includes search capabilities, now this page cannot sit in the layouts folder so it has to be a site page.

So go ahead and create a site page with a code behind follow this post to create one. The code behind is essential to change the status code of the page from 200 to 403. Now once that's created add the following onload code to change the status code to 403:

using System;
using System.Runtime.InteropServices;
using Microsoft.SharePoint.Publishing;
using System.Net;
using Microsoft.SharePoint;

namespace CustomErrorPage.Custom401
{
    [Guid("f255e09b-bc71-479a-b1d5-2c720e492748")]
    public class CustomError401 : PublishingLayoutPage
    {
        protected override void OnLoad(EventArgs e)
        {
            base.OnLoad(e);

            Response.StatusCode = (int)HttpStatusCode.Forbidden;
        }
    }
}


with that complete, make sure that once your Custom error page is deployed that you check it in and approve it, either through your event receiver or manually through your SharePoint Ribbon.

Now we also have to make a slight change in our http module from before. We where previously writing to the response our content but now we are just going to redirect to our new page.

private void WebApp_PreSendRequestContent(object sender, EventArgs e)
{
    HttpResponse response = _webApp.Response;

    string message = string.Empty;

    switch (response.StatusCode)
    {
        case 401:
            _webApp.Server.TransferRequest(@"/pages/CustomError403.aspx");
            break;
    }

}

Now SharePoint uses the 401 status code for it's own purposes so try and stay away from it, which is why I use the 403, in essence the same thing but it'll work.


A Better Approach


An alternative approach to the above, which I much prefer, is to set up your http handler to do both, swap in your custom error page, and also change it's status code.

using System;
using System.Net;
using System.Text.RegularExpressions;
using System.Web;

namespace ErrorRedirect
{
    public class ErrorSwapModule : IHttpModule
    {
        #region IHttpModule Members

        public void Dispose()
        {
            //throw new NotImplementedException();
        }

        public void Init(HttpApplication context)
        {
            //Before Contenet is sent, transfer Page based on status code
            context.PreSendRequestContent += (s, e) =>
            {
                var webApp = s as HttpApplication;

                if (webApp != null || webApp.Request != null || webApp != null)
                {
                    var res = webApp.Response;
                    var req = webApp.Request;
                    var usr = webApp.User;

                    var reg = new Regex("Custom40[1,4].aspx$");

                    if (!reg.IsMatch(req.Url.AbsolutePath))
                        if (res.StatusCode == 404)
                            webApp.Server.TransferRequest(@"/pages/Custom404.aspx");
                        else if (res.StatusCode == 401 && usr != null && !usr.Identity.IsAuthenticated)
                            webApp.Server.TransferRequest(@"/pages/Custom401.aspx");
                }
            };

            //after request, switch page status
            context.PostRequestHandlerExecute += (s, e) => {
                var webApp = s as HttpApplication;

                if(webApp != null)
                    if (webApp.Request.Url.AbsolutePath.Contains("Custom401"))
                        webApp.Response.StatusCode = (int)HttpStatusCode.Unauthorized;
                    else if (webApp.Request.Url.AbsolutePath.Contains("Custom404"))
                        webApp.Response.StatusCode = (int)HttpStatusCode.NotFound;   
            };

        }
        #endregion
    }
}



this transfers the page on the start of the request, and then at the end of it changes the status code to whatever you would like it to be. This eliminates the need to create a custom error site pages with code behinds. you can alternatively create your pages declaratively, programmatically or even manually, just make sure that the url's line up appropriately.

Now if you want to set up your Custom Error pages, add a module to your original project, that's the one that doesn't contain your http handler.

with that done expand your module in you solution explorer.


Notice that you have two files:

  • Elemetns.xml
  • Sample.txt

Basically the elements file is a set of instructions on how and where to deploy your sample.txt file, and yes that's silly why on earth would you want to deploy a text file? the answer is odds are you wouldn't instead we are going to deploy two site pages: Custom401 and Custom404. Now you can go through the pain of trying to change your Sample.txt file into an asp site page, or you can download the cks dev kit and make your life a lot less painful. In VS2012 go to tools and click Extensions and Updates:


next make sure you have Online selected in the left hand pane, in the right hand pane type in cks and hit enter, then pick the appropriate cks version (server or foundation, 2010 or 2012/201) and hit download


Once downloaded it'll prompt you for an install, just walk through it to completion. You'll have to restart visual studio for the changes to take effect, go ahead and do so.

Strangly enough i had to install the Cks - Development Tool Edition (server) for vs2010 to get my templates to show up, why i'm not sure but that's what i did.

now that you have your kit(s) installed, delete the sample.txt file and add two cks dev site pages.


Make one for 401 and one for 404 (make sure your running Visual Studio as an administrator)


now if you would like you can open those two up and modify them however you please, one thing that you definitely will want to do is change the master page from default.master to custom.master.

From
<%@ Page language="C#" MasterPageFile="~masterurl/default.master" Inherits="Microsoft.SharePoint.WebPartPages.WebPartPage,Microsoft.SharePoint,Version=14.0.0.0,Culture=neutral,PublicKeyToken=71e9bce111e9429c" %>

To
<%@ Page language="C#" MasterPageFile="~masterurl/custom.master" Inherits="Microsoft.SharePoint.WebPartPages.WebPartPage,Microsoft.SharePoint,Version=14.0.0.0,Culture=neutral,PublicKeyToken=71e9bce111e9429c" %>


this will give your site pages the same look and feel provided by your masterpage instead of the admin one 

but right now lets open up the elements file and actually deploy these pages to our web application.


out of the box you should see the above, we're going to change it to
<?xml version="1.0" encoding="utf-8"?>
<Elements xmlns="http://schemas.microsoft.com/sharepoint/">
  <Module Name="ErrorPages" Url="$Resources:cmscore,List_Pages_UrlName;" Path="ErrorPages">
    <File Path="Custom401.aspx" Url="Custom401.aspx" Type ="GhostableInLibrary" IgnoreIfAlreadyExists ="TRUE">
      <Property Name="Title" Value="401" />
      <Property Name="ContentType"
                Value="$Resources:cmscore,contenttype_pagelayout_name;" />
      <Property Name="PublishingPreviewImage"
                Value="~SiteCollection/_catalogs/masterpage/$Resources:core,Culture;/Preview Images/DefaultPageLayout.png, ~SiteCollection/_catalogs/masterpage/$Resources:core,Culture;/Preview Images/DefaultPageLayout.png" />
      <Property Name="PublishingAssociatedContentType"
                Value=";#$Resources:cmscore,contenttype_articlepage_name;;#0x010100C568DB52D9D0A14D9B2FDCC96666E9F2007948130EC3DB064584E219954237AF3900242457EFB8B24247815D688C526CD44D;#"/>
      <AllUsersWebPart WebPartZoneID="Left" WebPartOrder="1">
        <![CDATA[
      <WebPart xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns="http://schemas.microsoft.com/WebPart/v2">
        <Title>Content Editor</Title>
        <FrameType>None</FrameType>
        <Description>Allows authors to enter rich text content.</Description>
        <IsIncluded>true</IsIncluded>
        <ZoneID>Main</ZoneID>
        <PartOrder>0</PartOrder>
        <FrameState>Normal</FrameState>
        <Height />
        <Width />
        <AllowRemove>true</AllowRemove>
        <AllowZoneChange>true</AllowZoneChange>
        <AllowMinimize>true</AllowMinimize>
        <AllowConnect>true</AllowConnect>
        <AllowEdit>true</AllowEdit>
        <AllowHide>true</AllowHide>
        <IsVisible>true</IsVisible>
        <DetailLink />
        <HelpLink />
        <HelpMode>Modeless</HelpMode>
        <Dir>Default</Dir>
        <PartImageSmall />
        <MissingAssembly>Cannot import this Web Part.</MissingAssembly>
        <PartImageLarge>/_layouts/images/mscontl.gif</PartImageLarge>
        <IsIncludedFilter />
        <Assembly>Microsoft.SharePoint, Version=14.0.0.0, Culture=neutral, PublicKeyToken=71e9bce111e9429c</Assembly>
        <TypeName>Microsoft.SharePoint.WebPartPages.ContentEditorWebPart</TypeName>
        <ContentLink xmlns="http://schemas.microsoft.com/WebPart/v2/ContentEditor" />
        <Content xmlns="http://schemas.microsoft.com/WebPart/v2/ContentEditor" />
        <PartStorage xmlns="http://schemas.microsoft.com/WebPart/v2/ContentEditor" />
      </WebPart>
        ]]>
      </AllUsersWebPart>
    </File>
    <File Path="Custom404.aspx" Url="Custom404.aspx" Type ="GhostableInLibrary" IgnoreIfAlreadyExists ="TRUE">
      <Property Name="Title" Value="404" />
      <Property Name="ContentType"
                Value="$Resources:cmscore,contenttype_pagelayout_name;" />
      <Property Name="PublishingPreviewImage"
                Value="~SiteCollection/_catalogs/masterpage/$Resources:core,Culture;/Preview Images/DefaultPageLayout.png, ~SiteCollection/_catalogs/masterpage/$Resources:core,Culture;/Preview Images/DefaultPageLayout.png" />
      <Property Name="PublishingAssociatedContentType"
                Value=";#$Resources:cmscore,contenttype_articlepage_name;;#0x010100C568DB52D9D0A14D9B2FDCC96666E9F2007948130EC3DB064584E219954237AF3900242457EFB8B24247815D688C526CD44D;#"/>
      <AllUsersWebPart WebPartZoneID="Left" WebPartOrder="1">
        <![CDATA[
      <WebPart xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns="http://schemas.microsoft.com/WebPart/v2">
        <Title>Content Editor</Title>
        <FrameType>None</FrameType>
        <Description>Allows authors to enter rich text content.</Description>
        <IsIncluded>true</IsIncluded>
        <ZoneID>Main</ZoneID>
        <PartOrder>0</PartOrder>
        <FrameState>Normal</FrameState>
        <Height />
        <Width />
        <AllowRemove>true</AllowRemove>
        <AllowZoneChange>true</AllowZoneChange>
        <AllowMinimize>true</AllowMinimize>
        <AllowConnect>true</AllowConnect>
        <AllowEdit>true</AllowEdit>
        <AllowHide>true</AllowHide>
        <IsVisible>true</IsVisible>
        <DetailLink />
        <HelpLink />
        <HelpMode>Modeless</HelpMode>
        <Dir>Default</Dir>
        <PartImageSmall />
        <MissingAssembly>Cannot import this Web Part.</MissingAssembly>
        <PartImageLarge>/_layouts/images/mscontl.gif</PartImageLarge>
        <IsIncludedFilter />
        <Assembly>Microsoft.SharePoint, Version=14.0.0.0, Culture=neutral, PublicKeyToken=71e9bce111e9429c</Assembly>
        <TypeName>Microsoft.SharePoint.WebPartPages.ContentEditorWebPart</TypeName>
        <ContentLink xmlns="http://schemas.microsoft.com/WebPart/v2/ContentEditor" />
        <Content xmlns="http://schemas.microsoft.com/WebPart/v2/ContentEditor" />
        <PartStorage xmlns="http://schemas.microsoft.com/WebPart/v2/ContentEditor" />
      </WebPart>
        ]]>
      </AllUsersWebPart>
    </File>

</Module>

</Elements>

seems like a lot eh? basically our module element has two file child elements:


those represent the two site pages we made, now if you expand one of the file elements and collapse the AllUserWebPart element you're looking at the properties required for your site page.


now in the web part zone we just include the Content Editor web part that allows authors to enter rich text content on the page, thus letting some sort of publisher to customize the content of your custom error pages, letting you not worry about wording.

Next What we are going to do is add a feature receiver, because we declaratively add a content editor webpart every time we deploy our solution we are going to get an extra content editor webpart, kind of annoying, so what we are going to do is remove it in the event receiver. right click on the feature and click the add event receiver button.

you should see something like

replace the commented out FeatureActivated Method with the following

public override void FeatureActivated(SPFeatureReceiverProperties properties)
{
    //Grab the site we are deploying to
    var site = properties.Feature.Parent as SPSite;

    //Make sure its actually a site
    if (site != null)
    {
        //get the sites root web
        var web = site.RootWeb;

        //Grab the root webs pages gallery
        var pages = web.GetList("Pages");

        //Itterate through all pages
        foreach (SPListItem li in pages.Items)
        {
            //create a check for pages that end in 401.aspx or 404.aspx
            var r = new Regex(@".40[1,4]\.aspx$");

            //Check if page url matches above regex
            if (r.IsMatch(li.Url))
            {
                SPFile page = li.File;

                try
                {
                    //Check out the page if need be
                    if (page.RequiresCheckout)
                        page.CheckOut();

                    //move through all webparts on page, remove all but the first one(declared by us)
                    //but only if it's a COntent Editor webpart
                    using (var mgr = page.GetLimitedWebPartManager(PersonalizationScope.Shared))
                        if (mgr.WebParts.Count > 1)
                            for (int i = mgr.WebParts.Count - 1; i > 0; i--)
                            {
                                var wp = mgr.WebParts[i];
                                if (wp.Title.Equals("Content Editor"))
                                    mgr.DeleteWebPart(mgr.WebParts[i]);
                            }

                    page.CheckIn("Checked in by feature reciever");
                    page.Approve("Approved By Feature Reciever");
                }
                catch (Exception)
                {
                    page.UndoCheckOut();
                }
            }
        }
    }

}

your also going to have to include the following using statements.

  • using System.Text.RegularExpressions;
  • using System.Web.UI.WebControls.WebParts;

now open up your feature and you should have something along the lines of


Ok, now deploy your project and your two Custom error pages should show up in the pages library of your root web.


To sum up your doing the following

  • Create custom error pages, can be through Code, the Ribbon or Designer
  • Create an http Module that ties into the asp.net pipeline and redirects based on errorcodes

Thursday, 6 March 2014

Custom error page for a public facing publishing portal

In the previous Post we created a web application and extended it to have a Anonymous Public Facing Door, that is an entry point that is Read only and Anonymous. At the end of that post we saw a lovely blank 401 unauthorized error page.

Not exactly user friendly, especially when you work in government and you have some crazy conspiracy theorist claiming he's found the web page about selling organs on the black market, when it's really just a bus route some coop student forgot to check back in.

So lets start by creating a new SharePoint Project

Hit Ok,


Make sure to select Farm solution and hit finish.

Now you Have your SharePoint Project, next what your going to do is create a class library, right click on your solution->add->New Project.

Select Windows Templates, pick Class Library, give your project a name and hit OK.

In the above I accidentally have framework 4.5 selected, for SharePoint 2010 you have to have 3.5 so make the change here or right click on your project and change it there.

Next your going to have to add a reference to the System.Web.dll. In your solution explorer right click on references and hit add References.

with that added go to your class, by default the file and class should be called class1.cs change that to something more appropriate, it's common to suffix whatever you call it with the word module.

next implement the IHttpModule interface,

using System;
using System.Web;

namespace ErrorRedirect
{
    public class ErrorSwapModule : IHttpModule
    {
        #region IHttpModule Members

        public void Dispose()
        {
            throw new NotImplementedException();
        }

        public void Init(HttpApplication context)
        {
            throw new NotImplementedException();
        }

        #endregion
    }
}


right click on IHttpModule and implement it implicitly, that'll get you the init and dispose functions. I wrap them in a region this is just for decoration and makes it easer to manage large classes. how you organize your code is really up to you and your team, you just have to pick what works for you and stick to it.

next we add a new event handler to our context to handle The PreSendRequestContent event.

using System;
using System.Web;

namespace ErrorRedirect
{
    public class ErrorSwapModule : IHttpModule
    {
        private HttpApplication _webApp;

        #region IHttpModule Members
        public void Dispose()
        {
            //throw new NotImplementedException();
        }

        public void Init(HttpApplication context)
        {
            _webApp = context;
            _webApp.PreSendRequestContent +=
                new EventHandler(WebApp_PreSendRequestContent);
        }

        #endregion

        private void WebApp_PreSendRequestContent(object sender, EventArgs e)
        {
            HttpResponse response = _webApp.Response;

            string message = string.Empty;

            switch (response.StatusCode)
            {
                case 401:
                    this._webApp.Response.Clear();
                    this._webApp.Response.Write(
                        @"<html>
                            <head />
                            <body>
                                This is a Custom 401
                            </body>
                        </html>");
                    break;
            }
        }
    }
}


this is where we check the status code of our response and then redirect our user to where ever we feel is appropriate.

Now we've done a lot of work, but we're not done yet, we still have to register this module in our GAC, and to do this your project has to be signed, what this means is a bit out of the scope of this post but think of it as giving it a unique secure-ish identifier.

To sign your project, right click on it and go to properties, on the left select the signing tab

now from the drop down select browse, next navigate to the SharePoint Project we created earlier at the start of this post and select that key.snk file

with that selected our solution is now signed, now we are going to need our modules public Token, the public token is an abstraction of the public key, if you don't know what that is don't worry about it too much it has to do with asynchronous Encryption and a privet-public key pair again outside of the scope of this, but you know enough to look it up yourself.

Now when it comes to getting our public Token, the best way is in this post, don't worry it works in VS 2012 too.


when you run your tool, you should get something like the above, but with a different public key and token. make note of this Public Key Token for later. In my case it's 06ae58ed1b6fa751 in your case it'll be something different.

Now we have enough to register our Module in our Web Config, now you should never modify your web config by hand, but to keep this post shorter we are going to break that rule. open up the web config for the extension of your site. In the previous post we created a web app with port 3333, then we extended it on port 4444. Make sure you make your modification for port 4444.

Your web config will be "C:\inetpub\wwwroot\wss\VirtualDirectories\4444"


now open that up in your favourite editor, personally I like notepad++, but that's me.
look for the line
<modules runAllManagedModulesForAllRequests="true">
it should be around line 450 in the webconfig, now directly after it your going to append something along the lines of
<add name="ErrorRedirect"

    type="ErrorRedirect.ErrorSwapModule,
ErrorRedirect
,
Version=1.0.0.0,
Culture=neutral,
PublicKeyToken=06ae58ed1b6fa751"
/>

Just match the above with the below

using System;
using System.Web;

namespace ErrorRedirect
{
    public class ErrorSwapModule: IHttpModule
    {
        ...
    }

}

As for the Version number and Culture, you set that in the project properties
and we talked above how to get the Public key token.

with that change made in your webconfig, deploy your project and try to navigate to your settings folder of your extended web app that's the one with 4444 as a port number.


still not the greatest but you can at least customize it, now you could deploy a much more robust html page, read it in using some sort of string reader, and write that to your response, just make sure all of your assets, such as css and images are in a publicly available library, ie not your layouts folder.

Sunday, 2 March 2014

Create Anonymous Publishing Site

Lets Start with a regular Publishing Web app


notice the following:

  • Authentication: leave as default set to Classic
  • Name: Whatever you want it to be
  • Port: something easy to remember
  • Anonymous: Enabled.

and hit OK, wait a while and create your publishing site collection


notice the following
  • Name: whatever you like
  • Template Category: Publishing
  • Template type: Publishing Portal
  • primary admin: yourself
  • secondary admin: Local administrator 
hit OK, let it spin a while and you have your Web Application created.

next we're going to extend our web application, select our web application form central admin and hit the extend button.

next you'll get the Extend web application to another iis website screen


put in your info
  • Name
  • Port
  • Enable Anonymous 
  • Zone to internet
Scroll down and hit OK.

now click the authentication providers button

this will list the authentication providers, click on the internet one


with that selected change the authentication by unchecking the integrated windows authentication option


This will ensure that through this second door only anonymous unauthorized user ie the public can hit your publishing portal.

Finally navigate to the Site Permissions of the original web app set up  in this case it was the one with port 3333. Site Actions->Site Permissions

with that clicked the Anonymous Access Pane will pop up and select Entire Web Site

and hit OK, now navigate to your public facing extension in this case it had port 4444


and there you go your extended site. if you click sign in you'll get a 401 Unauthorized


this is because on your extension you enabled anonymous and disabled integrated windows authentication, meaning you couldn't log into your web application through this "Door" if you wanted to, ideal for a public facing portal.