Federated Authentication with Sitecore and the Windows Identity Foundation


UPDATE : There is a new version of the module here:  https://marketplace.sitecore.net/en/Modules/ADFSAuthenticator.aspx

 

Over the past few months I’ve done some work integrating Sitecore with multiple Federated Authentication systems like Ping Identity, ADFS and some home grown ones.  The way Federated Authentication works is instead of logging directly into an application the application sends the user to another system for authentication. Once that system authenticates the user an encrypted token, typically SAML, is passed back to the requesting application containing credentials and other information, known as claims.

Getting everything working with the Windows Identity Foundation proved to be more challenging than some of the other API’s I worked with. I’d like to thank Owain Jones, Kern Herskind Nightingale and Ivan Sharamok for their help and suggestions that made this all work.

Here is the source and update file for this solution. There is also this sample web.config.

Step 1: Setting up your development environment for claims based authentication.

This tutorial is already written so I’m just going to point to that. I suggest you set up a basic website and get claims Authentication working there before you go and try integrating it with Sitecore.

http://www.codeproject.com/Articles/278940/Claim-based-Authentication-and-WIF-Part-2

Step 2: Add Roles to Sitecore

Login To Sitecore and create the following roles :

Extranet/Registered Users

Sitecore/Author

Assign Sitecore Author to the Sitecore Client Authoring Role so they can login to the system.

Step 3: Modify the mock STS to send the roles

After you have completed that tutorial modify the STS project and change the code in CustomSecurityTokenService.cs that writes out the claims to include two roles that exist in your Sitecore system.

protected override IClaimsIdentity GetOutputClaimsIdentity(IClaimsPrincipal principal, RequestSecurityToken request, Scope scope)

{

if (null == principal)

{

throw new ArgumentNullException("principal");

}

ClaimsIdentity outputIdentity = new ClaimsIdentity();

// Issue custom claims.

// TODO: Change the claims below to issue custom claims required by your application.

// Update the application's configuration file too to reflect new claims requirement.

outputIdentity.Claims.Add(new Claim(System.IdentityModel.Claims.ClaimTypes.Name, principal.Identity.Name));

outputIdentity.Claims.Add(new Claim(ClaimTypes.Role, "Registered Users"));

outputIdentity.Claims.Add(new Claim(ClaimTypes.Role, "Author"));

return outputIdentity;

}

Step 4: Install the FedAuthenticator update package.

Login to Sitecore as an admin

Browse to /sitecore/admin/updateinstallationwizard.aspx

Install the update file found here.

Step 5 : Using Visual Studio or FedUtil.exe update the web.config of your sitecore solution to point to your STS like you did in the tutorial found under Step 1.

see : http://www.codeproject.com/Articles/278940/Claim-based-Authentication-and-WIF-Part-2

Step 6: Modify the web.config file.

Under the Modules Section move the Authentication Modules so they fire after Nexus and replace the Session Module with the one in the FedAuthenticator Module.

<add type=”Sitecore.Nexus.Web.HttpModule,Sitecore.Nexus” name=”SitecoreHttpModule” />

      <addname=“WSFederationAuthenticationModule” type=”Microsoft.IdentityModel.Web.WSFederationAuthenticationModule, Microsoft.IdentityModel, Version=3.5.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35″ preCondition=”managedHandler” />

      <addname=“SessionAuthenticationModule” type=”FedAuthenticator.Authentication.WSSessionAuthenticationModule, FedAuthenticator”/>

<add type=”Sitecore.Resources.Media.UploadWatcher, Sitecore.Kernel” name=”SitecoreUploadWatcher” />

Change the Location information inside of the web.config file so it looks like the following:

</system.webServer>

<location path=”sitecore modules/Shell/FedAuthenticator/sso.aspx”>

<system.web>

<authorization>

<deny users=”?”/>

</authorization>

</system.web>

</location>

<system.web>

<!–<authorization>

<deny users=”?” />

</authorization>–>

<!– Continue to run Sitecore without script validations –>

If you now try to hit sitecore modules/Shell/FedAuthenticator/sso.aspx the user should be logged into Sitecore with the Author Role.

Step 7: Create a secured page in Sitecore.

Under the Home Item create a new Sample Item and name it ‘secured’.

Set the permissions up to break inheritance for Extranet/Everyone then grant read access and enable enheritance for the Extranet/Register Users role.

Step 8 : Test authentication to the front end of the website.

Log out of Sitecore:

Clear your cookies to remove the pre-existing token.

Browse to http://yourwebsite/secured

You should be redirected to the STS token issuer. Once you log in you will be brought back to your website and should see the secured item.

It may be helpful to add this code to your sample layout.aspx page so you can see the users information.

<div><%=Sitecore.Context.User.Name %></div>

<div>Is Virtual? <%=Sitecore.Context.User.RuntimeSettings.IsVirtual %></div>

<div>Roles:</div>

<%

foreach (var role in Sitecore.Context.User.Roles)

{

%>

<div><%=role.Name %></div>

<%

}

%>

Code Review

Ideally this solution would have had two processors and nothing else.

The TokenRequestor looks to see if access is denied to an item and sends the user to the STS with a request for a token if the user is not already authenticated.

The UserResolver looks for a token in a cookie, if it exists and the user is not authenticated we log the user in a virtual user.

That’s all I expected to need when I first wrote this but I was having trouble persisting user information.  This led us to the Sitecore support portal and we found we needed a custom authentication provider and we had to overwrite the WSSessionModule to get this to work.

The root of the problem is three fold:

  1. Both Sitecore and the Windows Identity Foundation are fighting over the threads user identity located at HttpContext.Current.Request.User.
  2. The Windows Identity Foundation does not allow you to just request and parse a token just using the API. It forces you to use the http modules. If I could do this without the modules there would be a lot less code.  This is the approach I took when integrating Ping Identity and it was much easier but my Ping Identity code only works with Ping Identity and this solution should work with a lot of other identity providers, including Ping Identity.
  3. The Sitecore Nexus API is obfuscated and I had trouble figuring out where/when the Identity was being set.

So, now here’s all the ‘other’ code that was added to get the two systems to play nice.

WSSessionAuthenticationModule was added to prevent some of the Session overriding. The binding to PostAuthenticateRequest was removed from initialize module and we override the Authenticate Request method so it exits the method if we already have a Sitecore.Context.User.

The FederatedAuthenticationProvider was also added to get the role and user information to persist. The Forms Authentication Provider uses a cookie for saving the value but this cookie wasn’t persisting when we logged in using SSO so we read the Session Token instead inside of the GetCurrentUser method of the Authentication Helper.

The login method was also changed to not write out a forms cookie.

The sso.aspx page is being used to log in to Sitecore.  The user is assigned to the roles based on the Sitecore Domain. That is why the page currently lives under the sitecore modules folder.

Things that could use improvement

  1. Currently the user is logged in on every request when a token exists. I’d like to be able to just parse the token the first time it’s sent from the STS and log the user in once but that didn’t work out.
  2. Single Sign Out is not yet implemented.
  3. This solution is currently only supporting virtual users.
Advertisements

Multiple Sites with One Authoring Domain in Sitecore


Sitecore allows you to manage multiple sites by binding a hostname to a root content item via the webconfig. This works fine in the delivery enviornment but for authoring you would also now need a seperate entry for each site if your site is dependent on the Sitecore.Context.Site at all.

Adding the configs settings isn’t a huge deal but if you have 50 sites you now need 50 config settings for the authoring urls, 50 for the editing urls, 100 dns entries and 50 authoring bookmarks for your authors.

I came up with the following approach to resolving the site in page editor and preview modes to help alleviate some of this pain. I can now use one authoring domain and when I push the Page Editor or Preview buttons Sitecore will resolve the site for the item.

To achieve this I did four things on top of the normal setup :

  • Added SiteResolver to resolve site based on Item if sc_itemid is in the Query String and we are in Page Editor or Preview mode.
  • Override the LinkProvider to include the sc_site in the Query String when in Page Editor or Preview Mode.
  • Turn Off Rendering.SiteResolving
  • Add A Site Entry for the “Authroing” site with the root of the home node.

Normal Setup
Root Content Items

IIS Site Bindings

Added Sites to web.config (notice host names and start items)


      <site name="website" hostName="training65" virtualFolder="/" physicalFolder="/" rootPath="/sitecore/content" startItem="/home" database="web" domain="extranet" allowDebug="true" cacheHtml="true" htmlCacheSize="10MB" registryCacheSize="0" viewStateCacheSize="0" xslCacheSize="5MB" filteredItemsCacheSize="2MB" enablePreview="true" enableWebEdit="true" enableDebugger="true" disableClientData="false" />
      <site name="site2"  hostName="site2.training" virtualFolder="/" physicalFolder="/" rootPath="/sitecore/content" startItem="/Site2" database="web" domain="extranet" allowDebug="true" cacheHtml="true" htmlCacheSize="10MB" registryCacheSize="0" viewStateCacheSize="0" xslCacheSize="5MB" filteredItemsCacheSize="2MB" enablePreview="true" enableWebEdit="true" enableDebugger="true" disableClientData="false" />
      <site name="site3"  hostName="site3.training" virtualFolder="/" physicalFolder="/" rootPath="/sitecore/content" startItem="/site3" database="web" domain="extranet" allowDebug="true" cacheHtml="true" htmlCacheSize="10MB" registryCacheSize="0" viewStateCacheSize="0" xslCacheSize="5MB" filteredItemsCacheSize="2MB" enablePreview="true" enableWebEdit="true" enableDebugger="true" disableClientData="false" />

With the setup above and I log into each domain and author the sites but I can’t author from one site and have the Site resolve correctly.
Authoring Multiple Sites from One Domain
First I add my new authoring site below my last site because I want the others to resolve first on the delivery side.

      <site name="website" hostName="training65" virtualFolder="/" physicalFolder="/" rootPath="/sitecore/content" startItem="/home" database="web" domain="extranet" allowDebug="true" cacheHtml="true" htmlCacheSize="10MB" registryCacheSize="0" viewStateCacheSize="0" xslCacheSize="5MB" filteredItemsCacheSize="2MB" enablePreview="true" enableWebEdit="true" enableDebugger="true" disableClientData="false" />
      <site name="site2"  hostName="site2.training" virtualFolder="/" physicalFolder="/" rootPath="/sitecore/content" startItem="/Site2" database="web" domain="extranet" allowDebug="true" cacheHtml="true" htmlCacheSize="10MB" registryCacheSize="0" viewStateCacheSize="0" xslCacheSize="5MB" filteredItemsCacheSize="2MB" enablePreview="true" enableWebEdit="true" enableDebugger="true" disableClientData="false" />
      <site name="site3"  hostName="site3.training" virtualFolder="/" physicalFolder="/" rootPath="/sitecore/content" startItem="/site3" database="web" domain="extranet" allowDebug="true" cacheHtml="true" htmlCacheSize="10MB" registryCacheSize="0" viewStateCacheSize="0" xslCacheSize="5MB" filteredItemsCacheSize="2MB" enablePreview="true" enableWebEdit="true" enableDebugger="true" disableClientData="false" />
      <site name="authoring" hostName="" virtualFolder="/" physicalFolder="/" rootPath="/sitecore/content" startItem="/home" database="web" domain="extranet" allowDebug="true" cacheHtml="true" htmlCacheSize="10MB" registryCacheSize="0" viewStateCacheSize="0" xslCacheSize="5MB" filteredItemsCacheSize="2MB" enablePreview="true" enableWebEdit="true" enableDebugger="true" disableClientData="false" />

Now if I login to editing.training domain it resolves to this authoring site that doesn’t contain a domain name. I can log in and Edit all three of my sites but if the sites are relying on anything contained in the Sitecore.Context.Site this information will be pointing to the Authoring Site and it will be wrong.

I remembered how the cross site links resolve to the correct domain when Rendering.SiteResolving is set to true so I reflected on that code and came up with a class (mostly copy pasta’d….including the code with the Go To Label) to resolve the Site for an Item without looking at the host name.

This first class is the Link Helper Class and it’s ResolveTargetSite method is what we need.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Sitecore.Web;
using Sitecore.IO;
using Sitecore.Sites;
using Sitecore.Data.Items;
using Sitecore.Diagnostics;

namespace Sitecore.Prototypes.Links
{
    public static class LinkHelper
    {
        private static Dictionary<string, SiteInfo> _siteResolvingTable;
        private static List<SiteInfo> _sites;
        private static readonly object _syncRoot = new object();
        private delegate string KeyGetter(SiteInfo siteInfo);

        public static SiteInfo ResolveTargetSite(Item item)
        {
            SiteContext site = Context.Site;
            SiteContext context2 = site;
            SiteInfo info = (context2 != null) ? context2.SiteInfo : null;
            if ((context2 != null) && item.Paths.FullPath.StartsWith(context2.StartPath))
            {
                return info;
            }
            Dictionary<string, SiteInfo> siteResolvingTable = GetSiteResolvingTable();
            string key = item.Paths.FullPath.ToLowerInvariant();
        Label_00AE:
            if (siteResolvingTable.ContainsKey(key))
            {
                return siteResolvingTable[key];
            }
            int length = key.LastIndexOf("/");
            if (length > 1)
            {
                key = key.Substring(0, length);
                goto Label_00AE;
            }
            return info;
        }

        private static Dictionary<string, SiteInfo> GetSiteResolvingTable()
        {
            List<SiteInfo> sites = SiteContextFactory.Sites;
            if (!object.ReferenceEquals(_sites, sites))
            {
                lock (_syncRoot)
                {
                    if (!object.ReferenceEquals(_sites, sites))
                    {
                        _sites = sites;
                        _siteResolvingTable = null;
                    }
                }
            }
            if (_siteResolvingTable == null)
            {
                lock (_syncRoot)
                {
                    if (_siteResolvingTable == null)
                    {
                        _siteResolvingTable = BuildSiteResolvingTable(_sites);
                    }
                }
            }
            return _siteResolvingTable;
        }

        private static Dictionary<string, SiteInfo> BuildSiteResolvingTable(IEnumerable<SiteInfo> sites)
        {
            Dictionary<string, SiteInfo> dictionary = new Dictionary<string, SiteInfo>();
            List<string> list = new List<string>();
            KeyGetter[] getterArray = new KeyGetter[] { info => FileUtil.MakePath(info.RootPath, info.StartItem).ToLowerInvariant() };
            foreach (KeyGetter getter in getterArray)
            {
                foreach (SiteInfo info in sites)
                {
                    if (!string.IsNullOrEmpty(info.HostName) && string.IsNullOrEmpty(GetTargetHostName(info)))
                    {
                        Log.Warn(string.Format("LinkBuilder. Site '{0}' should have defined 'targethostname' property in order to take participation in site resolving process.", info.Name), typeof(LinkProvider.LinkBuilder));
                    }
                    else if ((!string.IsNullOrEmpty(GetTargetHostName(info)) && !string.IsNullOrEmpty(info.RootPath)) && !string.IsNullOrEmpty(info.StartItem))
                    {
                        string key = getter(info);
                        if (!list.Exists(new Predicate<string>(key.StartsWith)))
                        {
                            dictionary.Add(key, info);
                            list.Add(key);
                        }
                    }
                }
            }
            return dictionary;
        }

        private static string GetTargetHostName(SiteInfo siteInfo)
        {
            if (!string.IsNullOrEmpty(siteInfo.TargetHostName))
            {
                return siteInfo.TargetHostName;
            }
            string hostName = siteInfo.HostName;
            if (hostName.IndexOfAny(new char[] { '*', '|' }) < 0)
            {
                return hostName;
            }
            return string.Empty;
        }

    }
}

Next I create a custom SiteResolver and place it after the normal SiteResolver in the begin request pipeline.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Web;
using Sitecore.Data;
using Sitecore.Data.Items;
using Sitecore.Web;
using Sitecore.Sites;

namespace Sitecore.Prototypes.Links
{
    public class SiteResolver : Sitecore.Pipelines.HttpRequest.SiteResolver
    {

        public override void Process(Pipelines.HttpRequest.HttpRequestArgs args)
        {
            if ((Sitecore.Context.PageMode.IsPreview || Sitecore.Context.PageMode.IsPageEditor) 
                && HttpContext.Current.Request.QueryString["sc_itemid"] != null)
            {
                Database db = Sitecore.Context.Database;
                if (db != null)
                {
                    Item item = db.SelectSingleItem(HttpContext.Current.Request.QueryString["sc_itemid"]);
                    if (item != null)
                    {
                        SiteInfo site = LinkHelper.ResolveTargetSite(item);
                        SiteContext siteContext = SiteContextFactory.GetSiteContext(site.Name);
                        UpdatePaths(args, siteContext);
                        Sitecore.Context.Site = siteContext;
                    }
                }
            }          
        }
    }
}

Add that to the httpRequestBeing pipeline right after the normal site resolver

        <processor type="Sitecore.Pipelines.HttpRequest.SiteResolver, Sitecore.Kernel" />
        <processor type="Sitecore.Prototypes.Links.SiteResolver, Sitecore.Prototypes.Links"/>

Now when I hit the Page Editor or Preview buttons the site is displayed using the correct Sitecore.Context.Site, but if I click on a link to another page it loses the context….

To fix this I added a custom link provider that appends the sc_site to the query string if we are in preview or pageditor mode.

Here is the class

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Sitecore.Sites;

namespace Sitecore.Prototypes.Links
{
    public class LinkProvider : Sitecore.Links.LinkProvider
    {

        public override string GetItemUrl(Data.Items.Item item, Sitecore.Links.UrlOptions options)
        {
            string url = base.GetItemUrl(item, options);
            if (Sitecore.Context.PageMode.IsPageEditor || Sitecore.Context.PageMode.IsPreview)
            {
                url = AppendOption(url, "&sc_site=" + LinkHelper.ResolveTargetSite(item).Name);
            }
            return url;
        }

        public override string GetDynamicUrl(Data.Items.Item item, Sitecore.Links.LinkUrlOptions options)
        {
            string url  = base.GetDynamicUrl(item, options);
            if (Sitecore.Context.PageMode.IsPageEditor || Sitecore.Context.PageMode.IsPreview)
            {
                url = AppendOption(url, "&sc_site=" + LinkHelper.ResolveTargetSite(item).Name);
            }
            return url;
        }

        public string AppendOption(string url, string option)
        {
            return url.Contains("?") ? url + option : url + "?" + option;
        }
    }
}

And here is where I overrode it in the config section

		<linkManager defaultProvider="authoring">
			<providers>
				<clear />
				<add name="sitecore" type="Sitecore.Links.LinkProvider, Sitecore.Kernel" addAspxExtension="false" alwaysIncludeServerUrl="true" encodeNames="true" languageEmbedding="asNeeded" languageLocation="filePath" shortenUrls="false" useDisplayName="false" />
        <add name="authoring" type="Sitecore.Prototypes.Links.LinkProvider, Sitecore.Prototypes.Links" addAspxExtension="false" alwaysIncludeServerUrl="true" encodeNames="true" languageEmbedding="asNeeded" languageLocation="filePath" shortenUrls="false" useDisplayName="false"  />
			</providers>
		</linkManager>

Now my last problem is that because Rendering.SiteResolving is set to true so my domain name is switching so we need to set that to false in the Authoring Environment while we probably want it true on the delivery side.

       <setting name="Rendering.SiteResolving" value="false" />

Notes

  • I wouldn’t expect this to work with the Shared Source DB Site Resolver and I’m not sure about the other Shared Source Multi-Site piece.
  • Posting code in wordpress is painfull.
  • So is copying from it so I’ll try to get a download up

Auto Complete in Razor MVC with JQuery and LINQ to Entities


I had to create an auto-complete text box this week in Razor and didn’t see anything specific to doing it in Razor so I thought I’d throw this out there. I would like to say this post for getting it to work in MVC was very helpful and it didn’t take much effort to move from that to Razor and the Entity Framework.

In my example below I’ll be auto populating the product name based on the existing product names in the database using LINQ to Entities.

Step 1 : Download the JQuery Auto-Complete Library from google code

http://code.google.com/p/jquery-autocomplete/downloads/list

Step 2: Extract the following files into your scripts directory

jquery.autocomplete.js
jquery.autocomplete.css
indicator.gif

You can move them around later but for now lets keep them all in one folder and modify the css file to point to our new location for the loading gif.

background : url(‘/scripts/indicator.gif’) right center no-repeat;

Step 3 : Build your controller class

I’m selecting the top 10, distinct where the product name contains the typed in text.


using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.Mvc;
using MyNameSpace.Models;

namespace MyNameSpace.Controllers
{
    public class AutoCompleteController : Controller
    {
        MyNameSpaceEntities mhgDb = new MyNameSpaceEntities();

        public ActionResult ProductName(string q)
        {
            var productNames = (from p in mhgDb.Products where p.ProductName.Contains(q) select p.ProductName).Distinct().Take(10);

            string content = string.Join<string>("\n", productNames);
            return Content(content);
        }

    }
}

Step 4:  Add the JS and CSS files to your view


<script src="@Url.Content("~/Scripts/jquery.autocomplete.js")" type="text/javascript"></script>
<link href="@Url.Content("~/Scripts/jquery.autocomplete.css")" rel="stylesheet" type="text/css" />

Step 5 : Bind the Textbox to the Controller


<div class="editor-label">
@Html.LabelFor(model => model.ProductName)
</div>
<div class="editor-field">
@Html.EditorFor(model => model.ProductName)
@Html.ValidationMessageFor(model => model.ProductName)
</div>

<script type="text/javascript">

$(document).ready(function () {
$("#ProductName").autocomplete('@Url.Action("ProductName", "AutoComplete")', { minChars: 3 });
});

</script>

Step 6 : Build and Test

That’s all, there’s some tips around styling and parameters for autocomplete in the link to the other blog post I linked to above. I’d be interested in seeing a better LINQ statement if anyone has one.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.Mvc;
using MyHobbyGear.Models; 

namespace MyHobbyGear.Controllers
{
public class AutoCompleteController : Controller
{
MyHobbyGearEntities mhgDb = new MyHobbyGearEntities();
//
// GET: /AutoComplete/

public ActionResult Manufacturer(string q)
{
var manufacturers = (from p in mhgDb.Products where p.ManufacturerName.Contains(q) select p.ManufacturerName).Distinct().Take(10);

string content = string.Join<string>(“\n”,manufacturers);
return Content(content);
}

public ActionResult ProductName(string q)
{
var productNames = (from p in mhgDb.Products where p.ProductName.Contains(q) select p.ProductName).Distinct().Take(10);

string content = string.Join<string>(“\n”, productNames);
return Content(content);
}

}
}

Meta Tags for public facing websites


I recently took an attempt at writing a web service that allowed me to get details about a web page similar to what you see when you paste a link in face book. In doing this I learned that there are quite a few meta tags a publicly facing site should have that go beyond the basic SEO tags we’ve been putting in for years.

The SEO tags are Title, Description, Keywords and Canonical.

Title is the title you see in the top of the browser window as well as the title search engines give to your page.

<title>My Page Title Goes Here</title>

Description
is not visible in the browser window but it used below the title of your page in search engines to describe your page.

<meta name=”description” content=”This is the best page in the world, you should visit it so you know what everyone else is talking about”>

Keywords keywords should contain a comma seperated list of the search terms you are targeting. They say the search engines no longer use this so you may want to save yourself some maintenance time and stop supporting this term.

<meta name=”keywords” content=”my keyword1, my keyword 2″/>

Canonical is a relatively new one that is for search engines. If you can access a page from multiple URL’s the canonical tag should be the most human readable version that you want the search engines to display.

If you support friendly urls

something like http://www.mywebsite.com/Articles/1/Title-Of-My-Story

is preferred over http://www.mywebsite.com/articles?id=1

In this case you would place the tag below in both versions of the page.

<link rel="canonical" href="http://www.mywebsite.com/Articles/1/Title-Of-My-Story">

Now the new stuff I’m seeing is used to describe your page to non search engines but things like Browsers, IPhones, Mashups and Facebook.

Shortcut_Icon used in favorites/bookmarks
sample from cnn.com
<link type=”image/x-icon” href=”http://i.cdn.turner.com/cnn/favicon.ico&#8221; rel=”shortcut icon”>

Apple Touch Icon points to hi-res icon for Apple iTouch Devices.
sample from stackoverlfow.com
<link href=”http://sstatic.net/stackoverflow/img/apple-touch-icon.png&#8221; rel=”apple-touch-icon”>

image_src is picked up by things like facebook and google news to show the image associated with your pages content.
sample from cnn.com article
<link rel=”image_src” href=”http://i.cdn.turner.com/cnn/.element/img/3.0/newsscanner/no_image_cnn_90x51.jpg“/>

Open Graph tags for facebook
If you care about what a link to your site looks like when someone puts a link up in facebook you can control it with open graph tags.
There is more info on open graph here on facebook.

Here’s a youtube video sample using open graph

    <meta property="og:url" content="http://www.youtube.com/watch?v=i4SkO3ypCXw" />
    <meta property="og:title" content="Drunken Master Trailer" />
    <meta property="og:description" content="Wong Fei Hong (Jackie Chan) is sent to train under his uncle, a wine guzzling master famous for crippling his students. Desperate to escape this brutal teach..." />
    <meta property="og:type" content="video" />
    <meta property="og:image" content="http://i2.ytimg.com/vi/i4SkO3ypCXw/default.jpg" />
    <meta property="og:video" content="http://www.youtube.com/e/i4SkO3ypCXw" />
    <meta property="og:video:type" content="application/x-shockwave-flash" />
    <meta property="og:site_name" content="YouTube" />

C# Webservice for getting Link details like facebook with Open Graph Support


Was working on customizing the CK Editor for a site I’m working on and thought after using sites like facebook where they just paste a link and it auto formats it with details people are not going to want to use the actual link button. That’s too much work. I started looking around for a CK Editor Plugin that did the ‘facebook magic’ for me but I couldn’t find one. So I started looking at facebook, digging through their api and decided I needed to build a webservice to do this.

Here’s what I did:

1.  Use HtmlAgilityPack for html parsing.
2.  Get the basic info from standard ‘head’ tags.
3. Check to see if the site supports facebooks ‘open graph’
4.  If we still don’t have an image loop though all the images on the page. Download them and check they are atleast 50 pixels, ‘score’ them based on the alt tag being compared to the title and h1 tag.
5. If the link is an image itself set the link type to 2. (Link type of 0 = non html or image.)

Read more of this post

.net MVC Validation with Data Annotation


With MVC you can validate data in the UI and before sending to the database auto-magically if you add Data Annaotation Validators to your model classes. Here are some of the more commonly used validators and samples on how to use them.

Add using System.ComponentModel.DataAnnotations; to your model.

[Required] – States the field is required

sample :
[Required( AllowEmptyStrings=false,ErrorMessage=”You must eneter a name”)]
public string Name { get; set; }

[StringLength] – Used to set min and max string length.

sample:
[StringLength (20,MinimumLength=5,ErrorMessage=”Name must be between 5 and 20 characters”)]

[MaxLength] – Max length of a string

sample:
[MaxLength(20,ErrorMessage=”Name must be less than 20 Characters”);
public string Name { get; set; }

[MinLength] – minimum length of a string

sample:
[MinLength (5,ErrorMessage=”Name must be more than 5 characters”)]
public string Name { get; set; }

[Range] – States the value of a number must be between x and y.

sample:
[Range (1,99,ErrorMessage=”Age must be between 1 and 99″)]
public int age {get;set;}

[RegularExpression] – Used to validate the format of a string

sample:
[RegularExpression(“[a-z0-9!#$%&’*+/=?^_`{|}~-]+(?:\\.[a-z0-9!#$%&’*+/=?^_`{|}~-]+)*@(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\\.)+[a-z0-9](?:[a-z0-9-]*[a-z0-9])?”, ErrorMessage=”Invalid Email Address”)]
public string Email { get; set; }

[PropertiesMustMatch] – Class level attribute used to validate something like Password1 = Password2.

sample:
[PropertiesMustMatch(“Password”, “ConfirmPassword”, ErrorMessage = “The password and confirmation password do not match.”)]
public class user {

public string name {get;set;}

public string Password {get;set;}

public string ConfirmPassword {get;set;}

}

Rich Text Editing with CKEditor, MVC 3 and the Razor Engine


I was creating a model /view for editing large chunks of text on my website with MVC and the Razor engine. I created everything following Sott Gu’s tutorial on how to build a music store. When all was said and done I had a 50 character wide single line textbox for editing a large html block and thought ok this isn’t I deal.  Here are the steps I took to get a rich text editor in there.

Most of this came from this post on code project but it’s a bit outdated and doesn’t have everything.

The FCK Editor became the CK Editor, I guess they opted to drop the F over adding a U.

Step 1  : Change to Text Area

You should  have something like this in your Create and Edit Views

@Html.EditorFor(model => model.Text)

Lets change that to :

@Html.TextAreaFor(model => model.Text, new { @class = “adminRichText” })

Step 2 : Widen the text area

In your Site.Css file add the folowing to widen the text area :

.adminRichText
{
width:450px;
}

Step 3 : Add the CK Editor (formerly FCK Editor) to your project

a. Download here

b. Create a ‘Javascript’ folder under ‘Content’ in your website.

c. Extract to /Javascript/Content.

Step 4 : Include the scripts in your View

Below the validation scripts add

<script type=”text/javascript” src=”@Url.Content(“~/content/javascript/ckeditor/ckeditor.js”)”></script>

<script type=”text/javascript” src=”@Url.Content(“~/content/javascript/ckeditor/adapters/jquery.js”)”></script>

Step 5 : Turn the text area into a Rich Text Box

At bottom of view after controls are initialized add:

<script language=”javascript” type=”text/javascript”>
jQuery(“.adminRichText”).ckeditor();
</script>

Step 6 : Enable Post Back of Rich Text for your Edit and Create Methods in your controller class

[HttpPost, ValidateInput(false)]
public ActionResult Create(AreaText areaText)

[HttpPost, ValidateInput(false)]
public ActionResult Edit(string id, FormCollection collection)

Step 7: Use Html.Raw to display the data

On your index view update the output area to decode the html.

item.Text

becomes

@Html.Raw(item.Text)

ckEditor in mvc

ckEditor in mvc

Note : There are some good suggestions in the comments on ways to continue using validation :

Option 1 from Micheal.
“In your CKEditor config file add the following line:
config.htmlEncodeOutput = true;”

Option 2 from Blair.
“I was able to get this to work without having to turn off validation by putting in
[AllowHtml] in the property definition”