Search, SharePoint

Add a sorted ‘show more’ link to your searchwebpart

You probably have a page somewhere with a webpart that rolls up some search result content from the rest of your SharePoint environment. And if you want to offer your users a ‘show more’ link to easily redirect them to all the results on your search page, you can change your display template to add a ‘show more’ link.
This can be a static link, but it is much nicer if you do this dynamically by retrieving the search query from your context.

However, this will just let you query the results without any custom sort. To make sure the results are in the same order that the user sees on the homepage you will also need to retrieve the selected sorting from the context. And to make matters more challenging, what if the webpart is configured to show the sort-dropdown so the user can switch sorting? We will make sure to support both in our template, starting with the fallbacksort which can be configured in the webpart’s query builder.

Because I am using the ResultScriptWebPart webpart in this example, I need to make a copy of the Control_SearchResults.html display template. This template is the wrapper for the search results and displays the sort-dropdown and the footer for the results. I renamed it Control_SearchResults_showmore.html.

Add the link and retrieve the query

You probably want to show the link at the bottom of your results. So go and find the ‘ms-srch-resultFooter’ div which contains the footer for the webpart.
We only want to display the link if there are more results than are shown in the webpart. So we will need to check the total returned rows (totalrows) are higher than the amount displayed (rowcount).

<!--#_
    /*** Start Show More ***/
    var totalrows = ctx.DataProvider.get_totalRows(); 
    var rowcount = ctx.DataProvider.get_resultsPerPage();

Then we build up the url to the search results page and retrieve the query from the context. Some characters needs to be replaced to make sure any encoding will not mess up our querystring.

    var searchurl = "/search/Pages/Everything.aspx?k="
    var searchquery = $htmlEncode(ctx.ListData.ResultTables[0].Properties.QueryModification);
    searchquery = searchquery.replace("&","%26");
    searchquery = searchquery.replace(new RegExp(""", 'g'), "%5C%22");
    var searchlink = searchurl + searchquery;
    if (!ctx.ClientControl.get_shouldShowNoResultMessage() && totalrows > rowcount)
    {
_#-->
                <a id="btnshowmore" style="float: right;" href="_#= searchlink =#_" target="_blank" rel="noopener noreferrer">show more</a>
<!--#_ } /*** End Show More ***/ _#-->

Get the configured sort and add this to the query

We are going to place the currently applied sort in a variable, ‘window.SelectedSort’.
Find the div ‘ResultHeader’. In here you will find the sort dropdown with id ‘SortBySel’. Around this div there is an ‘if’ statement (showSortOptions) to determine if the sort dropdown is shown.
We need to add an ‘else’ statement to this, so if the dropdown is not enabled, possibly the fallbacksort is enabled and needs to be used. So, below this existing ‘if’ statement:

    if(showSortOptions){
        ----snipped default code----
    }

Add the following ‘else’ statement:

    else
    {
        window.selectedSort = ctx.DataProvider.get_fallbackSort()[0];
    }

(Note:This example supports only one sort-level. Of course you can get more than one sort level by iterating through the sorts.)

Add support for the sort-dropdown

On the first load the current sortrankname (ctx.DataProfider.getSortRankName()) will be empty. So we need to check for this. If it is the first load, you can select the first available sort which is selected by default:

    if (ctx.DataProvider.getSortRankName() == null)
    {
        window.selectedSort = availableSorts[0].sorts[0];
    }

After selecting a different sort order, the availablesort will be filled and you can save the current sort to your parameter:

    if(ctx.DataProvider.getSortRankName() == cplxsort.name) 
    {
        window.selectedSort = cplxsort.sorts[0];
        ----snipped default code----
    }

Pick up the selected sort and add this to the searchlink. Again some characters need to be replaced to ensure encoding will not mess up our querystring.

    if (window.selectedSort != null)
    {
        searchlink = searchlink.replace("?k=", "#Default={%22k%22:%22");
        searchlink = searchlink + "%22,%22o%22:[{%22d%22:" + window.selectedSort.d + ",%22p%22:%22" + window.selectedSort.p + "%22}]}";
    }

All together now!

Combined this will make your header look like this:

                    if(showSortOptions){
                        var resultHeaderClassNoEncode = "ms-metadata";
                        var availableSorts = ctx.DataProvider.get_availableSorts();
    _#-->
            <div id="ResultHeader" class="_#= resultHeaderClassNoEncode =#_">
                <ul id="Actions">
                    <li id="Sortby">
                        <select title="_#= $htmlEncode(Srch.Res.rs_SortDescription) =#_" id="SortBySel" onchange="$getClientControl(this).sortOrRank(this.value);">
                            <!--#_
                                if (ctx.DataProvider.getSortRankName() == null)
                                {
                                window.selectedSort = availableSorts[0].sorts[0];
                                }
                                        for (var i = 0; i < availableSorts.length; i++) {
                                            var cplxsort = availableSorts[i];
                                            if(!$isNull(cplxsort)){
                                                if(ctx.DataProvider.getSortRankName() == cplxsort.name) {
                                                  window.selectedSort = cplxsort.sorts[0];
    _#-->
                            <option selected="selected" value="_#= $htmlEncode(cplxsort.name) =#_">
                                _#= $htmlEncode(cplxsort.name) =#_
                            </option>
                            <!--#_
                                                } else {
    _#-->
                            <option value="_#= $htmlEncode(cplxsort.name) =#_">
                                _#= $htmlEncode(cplxsort.name) =#_
                            </option>
                            <!--#_
                                                }
                                            }
                                        }
    _#-->
                        </select>
                    </li>
                </ul>
            </div>
            <!--#_
                    }
                    else
                    {
                        window.selectedSort = ctx.DataProvider.get_fallbackSort()[0];
                    }

And your footer:

<div class="ms-srch-resultFooter">
                <!--#_
                    /*** Start Show More ***/
                    var totalrows = ctx.DataProvider.get_totalRows(); 
                    var rowcount = ctx.DataProvider.get_resultsPerPage();

                    var searchurl = "/search/Pages/Everything.aspx?k="
                    var searchquery = $htmlEncode(ctx.ListData.ResultTables[0].Properties.QueryModification);
                    searchquery = searchquery.replace("&","%26");
                    searchquery = searchquery.replace(new RegExp(""", 'g'), "%5C%22");
                    var searchlink = searchurl + searchquery;

                    if (window.selectedSort != null)
                    {
                        searchlink = searchlink.replace("?k=", "#Default={%22k%22:%22");
                        searchlink = searchlink + "%22,%22o%22:[{%22d%22:" + window.selectedSort.d + ",%22p%22:%22" + window.selectedSort.p + "%22}]}";
                    }
    _#-->
                <!--#_
                    if (!ctx.ClientControl.get_shouldShowNoResultMessage() && totalrows > rowcount)
                    {
    _#-->
                <a id="btnshowmore" style="float:right;" href="_#= searchlink =#_" target="_blank">show more</a>
                <!--#_
                    }
                    /*** End Show More ***/
    _#-->

You can download the full displaytemplate html file here..

javascript, Search, SharePoint

Export SharePoint search results to CSV after refining

There are a lot of blogs out there which touch on how to export SharePoint search results. However my requirement was to export the results AFTER using the refinement panel to filter deep down into the people search. Fortunately I had previously dabbled a bit with extracting search parameters after refining search results.

A short recap of my previous blog:
A simple people search url can look like this:
https://{host}/search/Pages/peopleresults.aspx?k=test

prerefine

After using the filters the query transforms to this:

https://{host}/search/Pages/peopleresults.aspx?k=test#Default=%7B%22k%22%3A%22test%22%2C%22r%22%3A%5B%7B%22n%22%3A%22JobTitle%22%2C%22t%22%3A%5B%22%5C%22%C7%82%C7%824d617374657220546573746572%5C%22%22%5D%2C%22o%22%3A%22and%22%2C%22k%22%3Afalse%2C%22m%22%3Anull%7D%2C%7B%22n%22%3A%22Department%22%2C%22t%22%3A%5B%22%5C%22%C7%82%C7%8254657374696e672044657074%5C%22%22%5D%2C%22o%22%3A%22and%22%2C%22k%22%3Afalse%2C%22m%22%3Anull%7D%5D%7D

postrefine

You can still spot some of our keywords but mostly the result behind the hash is encoded gibberish.

Get the Query

First step is to pull the query from current querystring to get the information behind the hash  (#), after filtering results with the refiners the query gets transformed and added after the hash. If there is no hash, no transformation is needed, then collect the original query after the keyword ‘k’ parameter and return..

function changesearchquery() {
    var hash = "";
    var fullUrl = window.location.href;
    var i = fullUrl.indexOf("#");
    if (i > -1) {
        hash = fullUrl.substring(i);
    }
    else {
        var currentSearch = (location.href.indexOf("k=") > 0) ? location.href.split("k=")[1] : "";
        //if page count exists
        currentSearch = (currentSearch.indexOf("#s=") > 0) ? currentSearch.split("#s=")[0] : currentSearch;
        return currentSearch;
    }

Transform the Gibberish

Next step is to transform the query gibberish to usable search properties. Parse the result to JSON so we can start extracting the data.

    var hashQuery = [];
    var queryGroups = hash.split("#");
    for (var i = 1; i < queryGroups.length; i++) { if (queryGroups[i].length > 0) {
            var keyValue = queryGroups[i].split("=", 2);
            var key = keyValue[0];
            var encodedValue = keyValue[1];

            if (key === "Default") { // json string format
                var jsonStringValue = decodeURIComponent(encodedValue);
                var safejsonStringValue = jsonStringValue.replace("’", "\’").replace("'", "\'");
                var queryObject = JSON.parse(safejsonStringValue);
                hashQuery[key] = queryObject;
            }
            else if (key === "k") { // simple format
                hashQuery[key] = encodedValue;
            }
        }
    }

Rebuild your Query

Extract the search properties and rebuild the query as a usable search api call.

    var filterprops = '';
    if (hashQuery["Default"] !== undefined) { // json string format
        if (hashQuery["Default"].k !== undefined && hashQuery["Default"].k.length > 0) {
            filterprops += "\"" + hashQuery["Default"].k + "\"";
        }
        for (var i = 0; i < hashQuery["Default"].r.length; i++) {
            if (hashQuery["Default"].r[i].m !== null) {  // check if 'm' contains data
                for (var n = 0; n < hashQuery["Default"].r[i].t.length; n++) {
                    var keywordkey = hashQuery["Default"].r[i].n;
                    var keywordvalue = hashQuery["Default"].r[i].m[hashQuery["Default"].r[i].t[n]];
                    filterprops = addFilterProp(keywordkey, keywordvalue, filterprops);
                }
            }
            else {
                for (var n = 0; n < hashQuery["Default"].r[i].t.length; n++) {
                    var tvalue = hashQuery["Default"].r[i].t[n];
                    if (tvalue.indexOf('ǂ') !== -1) {
                        // value is HEX type
                        var keywordkey = hashQuery["Default"].r[i].n;
                        var keywordvalue = hex2a(tvalue);
                        filterprops = addFilterProp(keywordkey, keywordvalue, filterprops);
                    }
                    else {  // simple format
                        var keywordkey = hashQuery["Default"].r[i].n;
                        var keywordvalue = tvalue;
                        filterprops = addFilterProp(keywordkey, keywordvalue, filterprops);
                    }
                }
            }
        }
    }
    else if (hashQuery["k"] !== undefined) { // simple value (no json)
        filterprops += "\"" + hashQuery["k"] + "\"";
    }
    return filterprops;
}

function addFilterProp(keywordKey, keywordValue, filterProps) {
    if (!filterProps === '') {
        filterProps += " AND ";
    }
    filterProps += "\"" + keywordKey + "\":\"" + keywordValue + "\" ";
    return filterProps;
}
function hex2a(hexx) {
    var hex = hexx.toString();
    hex = hex.replace('"', '').replace('"', '').replace('ǂ', '').replace('ǂ', '');
    var str = '';
    for (var i = 0; i <= (hex.length - 2); i += 2) {
        str += String.fromCharCode(parseInt(hex.substring(i, i + 2), 16));
    }
    return str;
}

Query through REST

Run the query through the REST api (note: Max 500 items can be retrieved through the rest api)

function getSearchResultsUsingREST(queryText) {
    Results = {
        element: '',
        url: '',
        init: function (element) {
            Results.element = element;
            Results.url = _spPageContextInfo.webAbsoluteUrl + "/_api/search/query?querytext='" + queryText + "'&sourceid='bbb05724-f652-4b86-beff-af1284e8e789'&selectproperties='PersonellID,PreferredName,JobTitle,WorkEmail,WorkPhone,MobilePhone,Office1,Room,Office2,Room2,Practice,Team,KnowledgeGroup,ProfessionGroup,Jurisdiction,Secretary'";
        },
        load: function () {
            $.ajax(
                {
                    url: Results.url,
                    method: "GET",
                    headers: {
                        "accept": "application/json;odata=verbose"
                    },
                    success: Results.onSuccess,
                    error: Results.onError
                }
            );
        },

        onSuccess: function (data) {
            var results = data.d.query.PrimaryQueryResult.RelevantResults.Table.Rows.results;
            var propArray2 = ["Personell ID", "Name", "Job Title", "Work e-mail", "Work phone", "Mobile phone", "Primary office", "Primary room number", "Secondary office", "Secondary room number", "Practice group", "Team", "Knowledge group", "Profession group", "Jurisdiction", "Secretary"];
            var exportedResult2 = new Array();

            // Get only the required the managed properties.
            for (var j = 0; j < results.length; j++) {
                var obj = new Object;
                for (var i = 0; i < propArray2.length; i++) {
                    if (results[j].Cells.results[i + 2].Value !== null) {
                        if (results[j].Cells.results[i + 2].Value.match(/"|,/)) {
                            results[j].Cells.results[i + 2].Value = '"' + results[j].Cells.results[i + 2].Value + '"';
                        }
                    }
                    obj[propArray2[i]] = results[j].Cells.results[i + 2].Value ? results[j].Cells.results[i + 2].Value : "";
                }
                exportedResult2.push(obj);
            }
            showSave(ConvertToCSV(JSON.stringify(exportedResult2), propArray2), "ExportedSearchResult.csv", "text/csv; charset=UTF-8");
        },
        onError: function (err) {
            alert(JSON.stringify(err));
        }
    };
    Results.init($('#resultsDiv'));
    Results.load();
}

Save it!

And save and format as CSV:

function showSave(data, name, mimeType) {
    resultBlob = new Blob([data], { type: mimeType });
    if (window.navigator.userAgent.indexOf("MSIE ") > 0 || !!window.navigator.userAgent.match(/Trident.*rv:11./)) {
        navigator.msSaveBlob(resultBlob, name);
    }
    else //other browsers : Chrome/FireFox (Supported Data URIs)
    {
        //creating a temporary HTML link element (they support setting file names)
        var link = document.createElement('a');
        var url = URL.createObjectURL(resultBlob);
        link.setAttribute("href", url);
        link.setAttribute("download", "ExportedSearchResult.csv");
        link.style.visibility = 'hidden';
        document.body.appendChild(link);
        link.click();
        document.body.removeChild(link);
    }
}
function ConvertToCSV(objArray, headerArray) {
    var array = typeof objArray !== 'object' ? JSON.parse(objArray) : objArray;
    var str = '';

    // Create header row.
    var headerLine = '';
    for (var i = 0; i < headerArray.length; i++) {
        if (headerLine !== "") {
            headerLine += ',';
        }
        headerLine += headerArray[i];
    }
    str += headerLine + '\r\n';
    // Create CSV body.
    for (var l = 0; l < array.length; l++) {
        var line = '';
        for (var index in array[l]) {
            if (line !== '') {
                line += ',';
            }
            var isnum = /^\+?\d+$/.test(array[l][index]);
            if (isnum) {
                line += '"=""' + array[l][index] + '"""';
            }
            else {
                line += array[l][index];
            }
        }
        str += line + '\r\n';
    }
    return str;
}

All this can come together by adding a button to your page:

<div style="display:table-cell;">
<a id="idExportSearchResults" href="#">Export Result</a>
</div>

which calls:

function getExcel(ctx) {
    var a = document.getElementById("idExportSearchResults");
    var currentSearch = changesearchquery();
    var decodedCurrentSearch = decodeURIComponent(currentSearch);
    getSearchResultsUsingREST(decodedCurrentSearch);
}

Download the full js file here.

Search, SharePoint

Get SharePoint Search Parameters from URL Hash

This is part of a solution in which we allowed the end user to save search queries after using the refinement panel / paging / query changes in the search results page.

For this I needed to be able to access the search parameters from behind the ‘#’, the ‘hash’, in the url querystring.

When using the SharePoint search box, your url will usually look somewhat like this after you have performed a search:

http://server/pages/search.aspx?k=myquery

When you are in your search results page and you decide to change your query, your url will get a ‘hash’ add-on, resulting in something like this:

http://server/pages/search.aspx?k=myquery#k=mynewquery.

When you apply any filtering / refiners, or simply start paging your results, more encoded gibberish will be added:

In this case we filter on FileType: pdf, searchword ‘mynewquery’:

http://server/pages/search.aspx?k=myquery#Default=%7B%22k%22%3A%22mynewquery%22%2C%22r%22%3A%5B%7B%22n%22%3A%22FileType%22%2C%22t%22%3A%5B%22equals(%5C%22pdf%5C%22)%22%5D%2C%22o%22%3A%22or%22%2C%22k%22%3Afalse%2C%22m%22%3Anull%7D%5D%7D

There is no way to extract these new search parameters from behind the ‘hash’, or ‘#’, from serverside code, since anything behind a ‘#’ is clientside and will not be sent to the server.

You can however use javascript. But, the markup behind the # can differ quite a lot depending on the field type being filtered. So, some transformation is necessary.

To start off, we extract the querystring and parse the value to JSON, values are placed in an Array.

    var hash = "";
    var fullUrl = window.location.href;
    var i = fullUrl.indexOf("#");
    if (i > -1)
        hash = fullUrl.substring(i);

    var hashQuery = [];

    var queryGroups = hash.split("#");
    for (var i = 1; i < queryGroups.length; i++) {         if (queryGroups[i].length > 0) {
            var keyValue = queryGroups[i].split("=", 2);
            var key = keyValue[0];
            var encodedValue = keyValue[1];

            if (key === "Default") { // json string format
                var jsonStringValue = decodeURIComponent(encodedValue);
                var safejsonStringValue = jsonStringValue.replace("’", "\’").replace("'", "\'");
                var queryObject = JSON.parse(safejsonStringValue);
                hashQuery[key] = queryObject;
            }
            else if (key === "k") { // simple format
                hashQuery[key] = encodedValue;
            }
        }
    }

After we collect the values, we need to make some sense of them so we can use them later on.

I have encountered an array (multi select filters), hex values (DocumentType), date ranges and simple key / values. Please see comments in code.

   if (hashQuery["Default"] != undefined) { // json string format
        for (var i = 0; i < hashQuery["Default"].r.length; i++) {
            if (hashQuery["Default"].r[i].m != null) {  // check if 'm' contains data
                for (var n = 0; n < hashQuery["Default"].r[i].t.length; n++) {
                    var keywordkey = hashQuery["Default"].r[i].n;
                    var keywordvalue = hashQuery["Default"].r[i].m[hashQuery["Default"].r[i].t[n]];
                }
            }
            else {
                for (var n = 0; n < hashQuery["Default"].r[i].t.length; n++) {
                    var tvalue = hashQuery["Default"].r[i].t[n];
                    if (tvalue.indexOf('ǂ') !== -1) {
                        // value is HEX type
                        var keywordkey = hashQuery["Default"].r[i].n;
                        var keywordvalue = hex2a(tvalue);
                        // your code here
                    }
                    else if (tvalue.indexOf('range(') !== -1) {
                        // value is a range (i.e. dates)
                        var rangestring = tvalue.match(/\((.+)\)/)[1];
                        var dates = rangestring.split(', ');
                        var minvalue = dates[0].split('T')[0]; // start of range
                        var maxvalue = dates[1].split('T')[0]; // end of range
                        if (minvalue !== "min") {  // check for actual date or minvalue
                            var mindate = new Date(dates[0]);
                            var day = mindate.getDate();
                            if (day < 10)
                                day = "0" + day;
                            var month = mindate.getMonth() + 1;
                            if (month < 10)
                                month = "0" + month;
                            var keywordkey = hashQuery["Default"].r[i].n;
                            var keywordmindate = day + "-" + month + "-" + mindate.getFullYear();
                            // your code here
                        }
                        if (maxvalue !== "max") {  // check for actual date or maxvalue
                            var maxdate = new Date(dates[1]);
                            var day = maxdate.getDate();
                            if (day < 10)
                                day = "0" + day;
                            var month = maxdate.getMonth() + 1;
                            if (month < 10)
                                month = "0" + month;
                            var keywordkey = hashQuery["Default"].r[i].n;
                            var keywordmaxdate = day + "-" + month + "-" + maxdate.getFullYear();
                            // your code here
                        }
                    }
                    else {  // simple format
                        var keywordkey = hashQuery["Default"].r[i].n
                        var keywordvalue = tvalue;
                        // your code here
                    }
                }
            }
        }
    }
    else if (hashQuery["k"] != undefined) { // simple value (no json)
        var keywordkey = "k";
        var keywordvalue = hashQuery["k"];
        // your code here
    }

For extracting the hex values add this function:

function hex2a(hexx) {
    var hex = hexx.toString();
    hex = hex.replace('"', '').replace('"', '').replace('ǂ', '').replace('ǂ', '');
    var str = '';
    for (var i = 0; i <= (hex.length - 2) ; i += 2) {         str += String.fromCharCode(parseInt(hex.substring(i, i + 2), 16));     }     return str; } 

Download the full .js file here.

Search, SharePoint

Retrieving Document Body Contents from the SharePoint Search Index

For my client I needed a solution to generate daily bulletins in word. These would be generated on the fly from search results containing documents (mostly word, pdf and aspx files) uploaded into SharePoint.

Please note that this solution is built for an on-premise installation of SharePoint 2013 where I have full control. This scenario is not meant for SharePoint online!

In my solution I have two types of document. One type is files which are imported without metadata, these can be any type but they are mostly office documents (word, powerpoint, excel) or pdf files. These are the files of which I want to extract the content of. The other type is uploaded via a form by a user, an aspx file is created and content is placed in the metadata, which means this file will already have the contents present in the metadata, stored in the field ‘aspx_Content’.

The challenge was to have the body text from the office documents and pdf files available for generating the bulletin on demand. The body text is stored in the field ‘ows_body’ mapped to managed metadata field ‘Contents’ in the SharePoint index for searching. I had hoped this would be available for retrieval also.

Unfortunately there is no way to ‘read’ from the ‘Contents’ or the ‘ows_body’ field. Even if you mark the property as retrievable nothing will be returned.
Mapping ‘ows_body’ to a new managed field will also always return empty. Seems there is no default way of retrieving content from the index.

So, not giving up, next step is to hook into the indexing process and try to get in before SharePoint prevents you getting the content. This can be done with the ‘Content Enrichment web service callout’.
I will not get into how to set up a content enrichment web service as Microsoft has already done this here: https://msdn.microsoft.com/en-us/library/office/jj163982.aspx.
The code used in our service first finds the ‘body’ property and stores it in the ‘MyContents’ managed property:

using Microsoft.Office.Server.Search.ContentProcessingEnrichment;
using Microsoft.Office.Server.Search.ContentProcessingEnrichment.PropertyTypes;
using System.Collections.Generic;
using System.Linq;
{
    public class ContentProcessingEnrichmentService : IContentProcessingEnrichmentService
    {
        public ProcessedItem ProcessItem(Item item)
        {
            ProcessedItem processedItem = new ProcessedItem
            {
                ItemProperties = new List&lt;AbstractProperty&gt;()
            };
            processedItem.ErrorCode = 0;
            processedItem.ItemProperties.Clear();
            try
            {
                var contentsProperty = item.ItemProperties.Where(p =&gt; p.Name == "body").FirstOrDefault();
                Property&lt;string&gt; MyContentsProp = contentsProperty as Property&lt;string&gt;;
                if (MyContentsProp == null)
                {
                    processedItem.ErrorCode = 1; // UnexpectedType
                    return processedItem;
                }
                else if (MyContentsProp.Value != null)
                {
                    MyContentsProp.Name = "MyContents";
                    processedItem.ItemProperties.Add(MyContentsProp);
                }
            }
            catch
            {
                processedItem.ErrorCode = 2; // UnexpectedError
            }
            return processedItem;
        }
    }
}

Create a new managed property in your Search Service Application, lets name it ‘MyContents’.
Make the field type ‘Text’ and set the checkbox ‘Retrievable’ to ‘true’.Since we already have contents in the aspx files we want to only use the enrichment service for non-aspx files. For aspx content we map this managed property to the already present property:
ows_aspx_Content’.

manpropStart up powershell to configure Search to use your Content Enrichment Service.

$snapin="Microsoft.SharePoint.PowerShell"
if (get-pssnapin $snapin -ea "silentlycontinue") {
}
else
{
	if (get-pssnapin $snapin -registered -ea "silentlycontinue") {
		Add-PSSnapin $snapin
		write-host -ForegroundColor Green "PSsnapin $snapin is now loaded"
	}
}
$ssa = Get-SPEnterpriseSearchServiceApplication -Identity "Search Service Application"
$config = New-SPEnterpriseSearchContentEnrichmentConfiguration
$config.Endpoint = "http://spcontentprocessing:8080/MyContentProcessingEnrichmentService.svc";
$config.InputProperties = "body";
$config.OutputProperties = "MyContents";
$config.SendRawData = $True;
$config.FailureMode = "WARNING";
$config.MaxRawDataSize = 51200;
$config.Trigger = '!IsMatch(FileExtension, "aspx")';
Set-SPEnterpriseSearchContentEnrichmentConfiguration -SearchApplication $ssa -contentEnrichmentConfiguration $config

We tell Search to only index contents from items that are not .aspx files (since we already have this content!) with this line:

$config.Trigger = '!IsMatch(FileExtension, "aspx")';

Perform a full crawl on your content source.
You can easily check your results by using the searchquerytool (http://sp2013searchtool.codeplex.com/), add your property to ‘Select Properties’ and run your search.

searchquerytool
‘view all properties’ lets you inspect the full contents:

allprops
If your field is not being returned check your SharePoint logfiles, any errors will be logged here. Now your document content is retrievable from the Search Index.

To retrieve your document content in programmatically:

string kwquery = string.Empty;
// i retrieve my documents by id, replace with your own query.
foreach (string indexdocid in indexdocids)
{
    if (!string.IsNullOrEmpty(indexdocid.Trim()))
    {
        kwquery += string.Format("IndexDocId={0} ", indexdocid);
    }
}
KeywordQuery kq = new KeywordQuery(SPContext.Current.Site);
kq.QueryText = kwquery;
// add the viewfields you want to return. you can also use MyContents for hithighlighting.
kq.SelectProperties.Add("UniqueID");
kq.SelectProperties.Add("Title");
kq.SelectProperties.Add("DocumentDate");
kq.SelectProperties.Add("Thema");
kq.SelectProperties.Add("MyContents");
ResultTableCollection results = new SearchExecutor().ExecuteQuery(kq);
foreach (ResultTable rt in results)
{
    DataTable dt = rt.Table;
    if (dt.Rows.Count &gt; 0)
    {
        dt = dt.DefaultView.ToTable();
        foreach (DataRow dr in dt.Rows)
        {
            string content = dr["MyContents"].ToString();
            // do something with your content
        }
    }
}
SharePoint

Getting the host page url in an app

Unfortunately it is not currently possible to retrieve the url of your app’s host page using standard tokens. Using document.referrer might work in an on prem environment, however online this will result in ‘/_layouts/15/silentSignIn.aspx’ regardless of which page you are on. After a lot of frustration (it seems so simple, it must be possible!!), searching and messing about I found the next workaround.

This should work both in an on premise as on O365, however one thing should be taken into account: When the app loads on a page for the very first time, it will fire some requests to retrieve the pages list and iterate through all the pages in this list. If you have huge page libraries, please don’t use this method. It will take too long, you might get timeouts or you might get inconsistent results depending on item limits. Also don’t use it for an app that you are planning to post in the store, as you have no control over the environment in which it will be used.

After the first load and successful retrieval of the url, the url is stored in the app part properties and will be readily available for future loads.

First, check your app part properties for any previous configuration. If none is present, then we will start finding the current page. (getPagesExecutor)

"use strict;";
var MaventionYammerFeedConfiguration = {
    SenderId: "", // Used to send postMessage
    HostWebUrl: "",
    AppWebUrl: "",
    CurrentPageUrl: "",
    WpId: "",
    HostWeb: "",
    PageType: {
        Welcome: "welcome",
        About: "about",
        Yammer: "yammer"
    },
    PagesBaseTemplate: "850",
    PagesListTitle: "Pages",
    PagesListId: 0,
    BoardMessage: ""
};
// This code runs when the DOM is ready
$(document).ready(function () {
    "use strict";
    // Get parameters from current app part web's url
    getParameters();
    if (MaventionYammerFeedConfiguration.WpId == "") {
        // app is not placed on a page so show about
        pageSelector(MaventionYammerFeedConfiguration.PageType.About);
    }
    else {
        if (MaventionYammerFeedConfiguration.CurrentPageUrl == "") {
            // app does not know parent page url
            getPagesExecutor();
        }
        else
        {
            // run the app
            dotheAppThing();
        }
    }
});
// Get parameters from the url tokens
function getParameters() {
    "use strict";
    var urlstring = location.search.replace("?", "");
    // App part token
    MaventionYammerFeedConfiguration.HostWebUrl = YammerUtility.getQueryStringParameter(urlstring, "SPHostUrl");
    MaventionYammerFeedConfiguration.AppWebUrl = YammerUtility.getQueryStringParameter(urlstring, "SPAppWebUrl");
    MaventionYammerFeedConfiguration.WpId = YammerUtility.getQueryStringParameter(urlstring, "wpId");
    MaventionYammerFeedConfiguration.CurrentPageUrl = YammerUtility.getQueryStringParameter(urlstring, "CurrentPageUrl");
    MaventionYammerFeedConfiguration.SenderId = YammerUtility.getQueryStringParameter(urlstring, "SenderId");
}

in getPagesExecutor we will load the required scripts to make some calls.

function getPagesExecutor() {
    var scriptbase = MaventionYammerFeedConfiguration.HostWebUrl + '/_layouts/15/';
    $.getScript(scriptbase + 'SP.Runtime.js',
        function () {
            $.getScript(scriptbase + 'SP.js',
                function () {
                    $.getScript(scriptbase + 'SP.RequestExecutor.js', getPagelist);
                });
        });
}

After which we will retrieve the pages library.

function getPagelist() {
    //Try to get the pages list from the host web
    //Get the URI decoded URLs.
    hostweburl = decodeURIComponent(MaventionYammerFeedConfiguration.HostWebUrl);
    appweburl = decodeURIComponent(MaventionYammerFeedConfiguration.AppWebUrl);
    executor = new SP.RequestExecutor(appweburl);
    executor.executeAsync({
        url: appweburl + "/_api/SP.AppContextSite(@target)/web/lists/?$filter=BaseTemplate eq " +
            MaventionYammerFeedConfiguration.PagesBaseTemplate + "&@target='" + encodeURIComponent(hostweburl) +
            "'", method: "GET", headers: { "Accept": "application/json; odata=verbose" },
            success: onGetHostWebPagesListSuccess,
            error: onGetHostWebPagesListFail
    });
}
function onGetHostWebPagesListFail(data, errorCode, errorMessage) {
    alert('Failed to get host web pages. Error:' + errorMessage);
    console.log("Data: " + data + "errorCode: " + errorCode + " errorMessage: " + errorMessage)
}

Initially, the value of MaventionYammerFeedConfiguration.PagesBaseTemplate is “850” which corresponds with the pages list associated with publishing infrastructure. If the result is empty we will proceed to check again with value “119” which corresponds with a sitepages list.

function onGetHostWebPagesListSuccess(data) {
    console.log(data.body); 
    var jsonObject = JSON.parse(data.body);
    var marketplaceHTML = ""; 
    var results = jsonObject.d.results;
    if (results.length === 0) {      
        if (MaventionYammerFeedConfiguration.PagesBaseTemplate = "850") {  // 850 pages list   
            //no pages list was found with basetemplate 850. change basetemplate.  
            MaventionYammerFeedConfiguration.PagesBaseTemplate = "119";  // 119 sitepages list      
            getPagesExecutor(); 
        }  
        else { 
            //no pages list was found with either basetemplate. 
            alert('Failed to find host page library.');   
        }
    }  
    else if (results.length > 1) {
        // more than one pages list present     
        alert('Found multiple host page libraries.');
    }
    else {
        if (results[0] != undefined) {
            // pages list found  
            MaventionYammerFeedConfiguration.PagesListTitle = results[0].Title;
            MaventionYammerFeedConfiguration.PagesListId = results[0].Id;
            getPagelistItems();
        }
    }
}

When successful, proceed to get the pages from the list.

function getPagelistItems() { 
    // get pages in pages list  
    hostweburl = decodeURIComponent(MaventionYammerFeedConfiguration.HostWebUrl);  
    appweburl = decodeURIComponent(MaventionYammerFeedConfiguration.AppWebUrl);
    executor = new SP.RequestExecutor(appweburl); 
    executor.executeAsync(      
        {         
            url: appweburl + "/_api/SP.AppContextSite(@target)/web/lists/getbyid('" + MaventionYammerFeedConfiguration.PagesListId + "')/items/?$select=FileRef&@target='" + encodeURIComponent(hostweburl) + "'",
            method: "GET", 
            headers: { "Accept": "application/json; odata=verbose" },  
            success: onGetHostWebPagesListItemsSuccess,  
            error: onGetHostWebPagesListFail    
        });
}

After retrieving the pages, iterate through them.

function onGetHostWebPagesListItemsSuccess(data) { 
    console.log(data.body); 
    var jsonObject = JSON.parse(data.body); 
    var marketplaceHTML = ""; 
    var results = jsonObject.d.results;  
    if (results.length === 0) {    
        //no pages found in list      
        alert('Failed to find host page items.');  
    }  
    else {  
        MaventionYammerFeedConfiguration.BoardMessage += "\nthis wpid: " + MaventionYammerFeedConfiguration.WpId + "\n";  
        //iterate pages     
        for (var i = 0; i < results.length; i++) {       
            // check if current app part is located on the current page.  
            getWebPartbyID(results[i].FileRef);
        }   
    }
}

Checking each page for presence of the current app part id.

function getWebPartbyID(pageurl) { 
    // check if current app part is located on the current page.    
    hostweburl = decodeURIComponent(MaventionYammerFeedConfiguration.HostWebUrl);   
    appweburl =  decodeURIComponent(MaventionYammerFeedConfiguration.AppWebUrl);  
    executor = new SP.RequestExecutor(appweburl);   
    executor.executeAsync(   
        {       
            url: appweburl + "/_api/SP.AppContextSite(@target)/web/GetFileByServerRelativeUrl('" + encodeURIComponent(pageurl) + "')/getLimitedWebPartManager()/webParts/getbycontrolid('" + MaventionYammerFeedConfiguration.WpId + "')/?$select=ID&@target='" + encodeURIComponent(hostweburl) + "'",   
            method: "GET",   
            headers: { "Accept": "application/json; odata=verbose" },        
            success: function (data) { ongetWebPartSuccess(pageurl, data); },    
            error: onGetWPFail  
        });
}
function onGetWPFail(data, errorCode, errorMessage) { 
    //current app part not present in current page, do nothing
}

When the app part id is present on the page, we have found our parent page.

function ongetWebPartSuccess(pageurl, data) {
    //current app part is present on current page   
    $("#boardcontent").text(pageurl);   
    var jsonObject = JSON.parse(data.body); 
    //get the app part guid   
    var wpGUID = jsonObject.d.Id;   
    //save the page url to app part properties.
    saveToAppPart(wpGUID, pageurl) }

Save the url in the app part properties, and proceed to run the rest of your apps functionality.

function saveToAppPart(wpGUID, pageurl) { 
    var dfd = $.Deferred();  
    //get current app part properties  
    getWebPartProperties(wpGUID, pageurl).done(function (wpProps) {  
        var content = wpProps.get_item("CurrentPageUrl"); 
        content = JSON.stringify(pageurl);  
        //save current app part properties 
        saveWebPartProperties(wpGUID, { CurrentPageUrl: content }, pageurl).done(function () {    
            dfd.resolve()  
        }).fail(self.error);
    }).fail(self.error);   
    return dfd.promise(); 
}
//pass in the web part ID as a string (guid) 
function getWebPartProperties(wpId, pageurl) {  
    var dfd = $.Deferred();  
    var context;  
    var factory;   
    var appContextSite;  
    context = new SP.ClientContext(MaventionYammerFeedConfiguration.AppWebUrl); 
    factory = new SP.ProxyWebRequestExecutorFactory(MaventionYammerFeedConfiguration.AppWebUrl);  
    context.set_webRequestExecutorFactory(factory);  
    appContextSite = new SP.AppContextSite(context, MaventionYammerFeedConfiguration.HostWebUrl);   
    var limitedWebPartManager = appContextSite.get_web().getFileByServerRelativeUrl(pageurl).getLimitedWebPartManager(SP.WebParts.PersonalizationScope.shared); 
    var webparts = limitedWebPartManager.get_webParts();   
    context.load(webparts); 
    context.executeQueryAsync(Function.createDelegate(this, function () {  
        var webPartDef = null;  
        //find the web part on the page by comparing ID's      
        for (var x = 0; x < webparts.get_count() && !webPartDef; x++) {       
            var temp = webparts.get_item(x); 
            if (temp.get_id().toString() === wpId) {      
                webPartDef = temp;         
            }   
        }      
        //if the web part was not found  
        if (!webPartDef) {         
            dfd.reject("Web Part: " + wpId + " not found on page.");    
            return;     
        }       
        //get the web part properties and load them from the server      
        var webPartProperties = webPartDef.get_webPart().get_properties();
        context.load(webPartProperties);
        context.executeQueryAsync(Function.createDelegate(this, function () {
            dfd.resolve(webPartProperties, webPartDef, context);
        }),
        Function.createDelegate(this, function () {
            dfd.reject("Failed to load web part properties");
        }));
    }), Function.createDelegate(this, function () {
        dfd.reject("Failed to load web part collection");
    }));
    return dfd.promise();
} 
//pass in the web part ID and a JSON object with the properties to update
function saveWebPartProperties(wpId, obj, pageurl) {
    var dfd = $.Deferred();
    getWebPartProperties(wpId, pageurl).done(
        function (webPartProperties, webPartDef, clientContext) {
            //set web part properties         
            for (var key in obj) {
                webPartProperties.set_item(key, obj[key]);
            }
            //save web part changes       
            webPartDef.saveWebPartChanges();
            //execute update on the server       
            clientContext.executeQueryAsync(Function.createDelegate(this, function () {
                dfd.resolve();
                MaventionYammerFeedConfiguration.CurrentPageUrl = pageurl;
                //run the app            
                dotheAppThing();
            }),
            Function.createDelegate(this, function () {
                dfd.reject("Failed to save web part properties");
            }));
        }).fail(function (err) {
            dfd.reject(err);
        });
    return dfd.promise();
}

Please note as explained before, this is not a solution in case of very large page libraries, but a good workaround when you have control over the max amount of pages which will be present in the pages list.

SharePoint

Custom Actions on the XSLT List View Ribbon

When you add a list or library view (xsltlistviewwebpart) on a page and choose to display the full toolbar, all buttons will become disabled. Including the vital ones like ‘new’ or ‘upload’ etc. rendering the use of views on your page, well, basically useless.

disabled

This can be solved by setting the toolbar to ‘none’ or ‘summary’. You then have a subset of buttons. However, if you are using custom actions on your list, these will also be stripped out. You are still able to use custom actions in the ECB menu but for operations on multiple items you want to have your custom actions on the ribbon.

summarytoolbar

Unfortunately, these two issues very much limit the use of custom actions in the ribbon for lists. Now they can only be used on the list or library page itself.

There is a workaround to still be able to use the custom actions bound to your list, however you cannot register these to your list by using Ribbon.ListItem.Actions or Ribbon.Documents.Manage. Doing so will cause them to be stripped out whenever choosing none or summary toolbar. Even if you position them in a new tab.

I placed my customactions in the ‘page’ tab (Ribbon.WikiPageTab) in a new group. This will always show your buttons, which was ok in our case. You could also choose to create a new tab. Just don’t bind them to your list.

pagetab

In your enabledscript you can specify when the buttons need to be enabled. For starters, when one or more items are selected:

var ci = CountDictionary(items);
if (ci &lt; 1)
{
    return false;
}

The next problem is that this has the disadvantage of not discriminating between list views and causing them to be enabled whenever an item was selected. For example my action which applies to a task list was also enabled when selecting a document from a library. This is solved by checking the list basetemplate id through ECMAScript when an item is selected. Note: I have previously created my own list definitions (id’s 88000 and 88001) so I can identify against their basetemplate id

try{
    var clientContext = SP.ClientContext.get_current();
    var web = clientContext.get_web();
    var list = web.get_lists().getById(SP.ListOperation.Selection.getSelectedList());
    clientContext.load(list);
    clientContext.executeQueryAsync(onQuerySucceeded, onQueryFailed);
    basetempl = list.get_baseTemplate().toString();
}
catch(err)
{
    return false;
}
return ((basetempl == '88000') || (basetempl == '88001'));

Resulting in buttons only responding to appropriate items being selected.

The full elements.xml for registering the buttons:
(Note: remember to also register your button to your list if you want to use this on the list/library page itself! Because this is on the page tab and will not be diplayed when viewing the list!)

<?xml version="1.0" encoding="utf-8"?>
<Elements xmlns="http://schemas.microsoft.com/sharepoint/">
  <CustomAction Id="Ribbon.WikiPageTab.ProjectGroupMove" Location="CommandUI.Ribbon">
    <CommandUIExtension>
      <CommandUIDefinitions>
        <CommandUIDefinition Location="Ribbon.WikiPageTab.Groups._children">
          <Group Id="Ribbon.WikiPageTab.ProjectGroupMove" Sequence="55" Description="Logboek item acties" Title="Logboek Acties" Command="EnableProjectGroupMove" Template="Ribbon.Templates.Flexible2">
            <Controls Id="Ribbon.WikiPageTab.ProjectGroupMove.Controls">
              <Button Id="Ribbon.WikiPageTab.ProjectGroupMove.MoveItem" Alt="Verplaats item(s) naar een dossier." Sequence="5" Command="MoveItem" Image32by32="_layouts/15/images/Client.SharePoint.Project/Project_move_32.png" Image16by16="_layouts/15/images/Client.SharePoint.Project/Project_move_16.png" LabelText="Verplaats Item(s)" TemplateAlias="o2" />
            </Controls>
            </Group>
          </CommandUIDefinition>
        <CommandUIDefinition Location="Ribbon.WikiPageTab.Scaling._children">
          <MaxSize Id="Ribbon.WikiPageTab.Scaling.ProjectGroup.MaxSize" Sequence="15" GroupId="Ribbon.WikiPageTab.ProjectGroupMove" Size="LargeLarge" />
        </CommandUIDefinition>
      </CommandUIDefinitions>
      <CommandUIHandlers>
        <CommandUIHandler Command="EnableProjectGroupMove" CommandAction="javascript:return true;" />
        <CommandUIHandler Command="MoveItem" CommandAction="javascript:ProjectMoveItem('{SiteUrl}');" EnabledScript="javascript:function oneOrMoreEnableMoveItem() {var items = SP.ListOperation.Selection.getSelectedItems();var ci = CountDictionary(items);if (ci < 1) { return false; } var basetempl = 00000; try{ var clientContext = SP.ClientContext.get_current(); var web = clientContext.get_web(); var list = web.get_lists().getById(SP.ListOperation.Selection.getSelectedList()); clientContext.load(list); clientContext.executeQueryAsync(onQuerySucceeded, onQueryFailed); basetempl = list.get_baseTemplate().toString(); } catch(err) { return false; } return ((basetempl == '88000') || (basetempl == '88001')); } function onQuerySucceeded() { } function onQueryFailed(sender, args) {alert('Request failed. ' + args.get_message() + '\n' + args.get_stackTrace());} oneOrMoreEnableMoveItem();" />
      </CommandUIHandlers>
    </CommandUIExtension>
  </CustomAction>
  <CustomAction Location="ScriptLink"ScriptSrc="/_layouts/15/Client.SharePoint.Project/ProjectCustomActions.js"Sequence="1" />
</Elements>
SharePoint

Retrieving Managed Navigation with CSOM

My collegue Waldek has written a nice post on reusing the Managed Navigation over site collections with use of javascript: http://www.mavention.com/blog/building-global-navigation-sharepoint-2013

I ran into the problem that if an end user does not have specific rights to the term group, the following popup occurs when trying to retrieve the navigation term set:

The following error has occured while loading global navigation: Cannot invoke method or retrieve property from null object. Object returned by the following call stack is null. “GetTermSet GetByName TermStores Microsoft.SharePoint.Taxonomy.TaxonomySession.GetTaxonomySession”

When checking the ‘intended use’ tab on my Term Set, the box ‘Use this Term Set for Site Navigation’ is checked. The box ‘Available for Tagging’ is unchecked since I do not want to use this term set for tagging.

Checking the ‘Available for Tagging’ box however makes the set available for use by end users, they can now retrieve the managed navigation term set with CSOM.

globalnavi

SharePoint

Redirect 404 to a PublishingPage in the Pages library

You can find lots of info on how to redirect a 404 error in your SharePoint environment to a custom page in the layouts folder, but in this case I needed to redirect to a page in the pages library, so local administrators could have freedom to edit the content on this page just like any other page.

The trick is to add a httpModule to catch the 404 status code and redirect to your custom 404 page. Do this before the request headers are set (by using PreSendRequestHeaders ), or you’ll get any of the following errors:

  • This operation requires IIS integrated pipeline mode
  • Error executing child request for /pages/404.aspx
  • Cannot redirect after HTTP headers have been sent

After implementing the redirect you’re not there yet. Code needs adding to catch any errors or looping that might occur.

Create your class, inherit from IHttpModule:

public class PageNotFoundModule : IHttpModule
    {
        LogManager lm = new LogManager();
        private HttpApplication application;
        private string pageNotFoundUrl = "/pages/404.aspx";
        private string pageErrorUrl = "/pages/Error.aspx";

        protected override void InitializeModule(HttpApplication context)
        {
            application = context;
            context.Error += new EventHandler(context_Error);
            context.PreSendRequestHeaders += new EventHandler(context_PreSendRequestHeaders);
        }

        void context_PreSendRequestHeaders(object sender, EventArgs e)
        {
            if (PageNotFoundModuleHelper.IsLooping)
            {
                return;
            }

            string pageName = string.Empty;
            HttpResponse response = application.Response;

            if (response.StatusCode == 404 && response.ContentType.Equals("text/html", StringComparison.CurrentCulture))
            {
                try
                {
                    string[] urlElements = PageNotFoundModuleHelper.RequestedUrl.Split(new char[] { '/' });
int pageElementIndex = urlElements.Length - 1;
                    pageName = urlElements[pageElementIndex].ToString();

                    if (!string.IsNullOrEmpty(WebPropertiesManager.GetWebPropertyValue(Constants.WebPropertyKeys.PageNotFound)))
                    {
                        pageNotFoundUrl = WebPropertiesManager.GetWebPropertyValue(Constants.WebPropertyKeys.PageNotFound);
                    }
                }
                catch (Exception ex)
                {
                    lm.LogException(ex);
                }

                if (!string.IsNullOrEmpty(pageNotFoundUrl))
                {
                    using (SPSite site = SPContext.Current.Site)
                    {
                        using (SPWeb web = site.RootWeb)
                        {
                            SPFile file = web.GetFile(pageNotFoundUrl);
                            if (file.Exists)
                            {
                                response.Redirect(pageNotFoundUrl + "?requestedUrl=" + PageNotFoundModuleHelper.RequestedUrl + "&requestedPage=" + pageName);
                            }
                        }
                    }
                }
            }
        }
        void context_Error(object sender, EventArgs e)
        {
            if (ConfigurationManager.AppSettings.HasKeys())
            {
                string bypassErrorPageValue = ConfigurationManager.AppSettings[Constants.Settings.AppSettings.BypassErrorPage];
                if (!string.IsNullOrEmpty(bypassErrorPageValue))
                {
                    bool bypassErrorPage;
                    if (bool.TryParse(bypassErrorPageValue, out bypassErrorPage))
                    {
                        if (bypassErrorPage)
                        {
                            return;
                        }
                    }
                }
            }

            HttpContext Context = HttpContext.Current;
            if (PageNotFoundModuleHelper.IsLooping)
            {
                return;
            }
            Exception[] unhandledExceptions = Context.AllErrors;
            foreach (Exception ex in unhandledExceptions)
            {
                lm.LogException(ex);
            }

            Exception exception = PageNotFoundModuleHelper.Exception;
            if (PageNotFoundModuleHelper.StatusCode != 404 && exception != default(Exception))
{
                lm.LogException(exception);
                try
                {
                    if (!string.IsNullOrEmpty(WebPropertiesManager.GetWebPropertyValue(Constants.WebPropertyKeys.PageError)))
                    {
                        pageErrorUrl = WebPropertiesManager.GetWebPropertyValue(Constants.WebPropertyKeys.PageError);
                    }
                }
                catch (Exception ex)
                {
                    lm.LogException(ex);
                }
                if (!string.IsNullOrEmpty(pageErrorUrl))
                {
                    using (SPSite site = SPContext.Current.Site)
                    {
                        using (SPWeb web = site.RootWeb)
                        {
                            SPFile file = web.GetFile(pageNotFoundUrl);
                            if (file.Exists)
                            {
                                application.Response.Redirect(pageNotFoundUrl + "?requestedUrl=" + PageNotFoundModuleHelper.RequestedUrl);
                            }
                        }
                    }

                }
            }
        }
    }

    /// <summary>
    /// Class containing properties for easy access, the properties are used in the PageNotFoundModule
    /// </summary>
    public static class PageNotFoundModuleHelper
    {
        private const string ReferrerUrlQueryStringParameterName = "ReferrerUrl";
        private const string RequestedUrlQueryStringParameterName = "RequestedUrl";

        #region helper properties
        /// <summary>
        /// Gets the last unhandled Exception.
        /// </summary>
        public static Exception Exception
        {
            get
            {
                Exception result = default(Exception);

                if (HttpContextObjectAvailable)
                {
                    if (HttpContext.Current.Server != null)
                    {
                        Exception exception = HttpContext.Current.Server.GetLastError();
                        if (exception != null)
                        {
                            result = exception;

                            if (exception is HttpUnhandledException)
                            {
                                Exception innerException = exception.InnerException;
                                if (innerException != null)
                                {
                                    result = innerException;
                                }
                            }
                        }
                    }
                }
                return result;
            }
        }
        /// <summary>
        /// Gets a value indicating whether the requested url is the same as the referrer url.
        /// </summary>
        /// <value>
        /// 	<c>true</c> if this instance is looping; otherwise, <c>false</c>.
        /// </value>
        public static bool IsLooping
        {
            get
            {
                bool result = false;

                if (string.IsNullOrEmpty(RequestedUrl) && string.IsNullOrEmpty(ReferrerUrl))
                {
                    return true;
                }

                if (!string.IsNullOrEmpty(RequestedUrl) && !string.IsNullOrEmpty(ReferrerUrl))
                {
                    result = RequestedUrl.Equals(ReferrerUrl, StringComparison.OrdinalIgnoreCase);
                }
                return result;
            }
        }

        /// <summary>
        /// Gets the referrer url if it is available or an empty string.
        /// </summary>
        public static string ReferrerUrl
        {
            get
            {
                string result = String.Empty;

                if (RequestObjectAvailable)
                {
                    string referrerUrl = HttpContext.Current.Request.QueryString[ReferrerUrlQueryStringParameterName];
                    if (referrerUrl != null)
                    {
                        result = referrerUrl;
                    }
                    else if (HttpContext.Current.Request.UrlReferrer != null)
                    {
                        result = HttpContext.Current.Request.UrlReferrer.ToString();
                    }
                }

                return result;
            }
        }

        /// <summary>
        /// Indicates if the current request is successful.
        /// </summary>
        public static bool RequestOk
        {
            get
            {
                bool result = true;
                if (HttpContextObjectAvailable)
                {
                    result = StatusCode == 200 || !(HttpContext.Current.Handler is Page);
                }
                return result;
            }
        }

        /// <summary>
        /// Gets the status code of the current request.
        /// </summary>
        public static int StatusCode
        {
            get
            {
                int result = 0;
                if (ResponseObjectAvailable)
                {
                    result = HttpContext.Current.Response.StatusCode;
                }
                return result;
            }
        }

        /// <summary>
        /// Indicates if the current HttpResponse object is available.
        /// </summary>
        private static bool ResponseObjectAvailable
        {
            get
            {
                return HttpContextObjectAvailable && HttpContext.Current.Response != null;
            }
        }

        /// <summary>
        /// Gets the requested url if it is available or an empty string.
        /// </summary>
        public static string RequestedUrl
        {
            get
            {
                string result = String.Empty;

                if (RequestObjectAvailable)
                {
                    string requestedUrl = HttpContext.Current.Request.QueryString[RequestedUrlQueryStringParameterName];
                    if (requestedUrl != null)
                    {
                        result = requestedUrl;
                    }
                    else if (HttpContext.Current.Request.Url != null)
                    {
                        result = HttpContext.Current.Request.Url.ToString();
                    }
                }
                return result;
            }
        }

        /// <summary>
        /// Indicates if the current HttpRequest object is available.
        /// </summary>
        private static bool RequestObjectAvailable
        {
            get
            {
                return HttpContextObjectAvailable && HttpContext.Current.Request != null;
            }
        }

        /// <summary>
        /// Indicates if the current HttpContext object is available.
        /// </summary>
        private static bool HttpContextObjectAvailable
        {
            get
            {
                return HttpContext.Current != null;
            }
        }

        #endregion

    }

Add your module to the web.config in the <httpModules> section:

<add name=”PageNotFoundModule” type=”Client.Project.Core.Framework.PageNotFoundModule, Client.Project.Core, Version=1.0.0.0, Culture=neutral, PublicKeyToken=898c2b280acc4521″ />

Office 365, SharePoint

SharePoint Editions and Office 365 comparison

I needed an easy feature comparison sheet for SharePoint 2010 editions (Foundation, Server Standard and Enterprise) and SharePoint Online in Office 365. For this I used the very helpful Editions Comparison page by Microsoft plus the Office 365 Beta SharePoint Online Standard Beta Description and poured all the info into a table.

Please note that I used the Beta Service Description to get my info, this is just a summary and the info might be subject to change.

Foun-
dation
Stan-
dard
Enter-
prise
365
Accessibility
Blogs
Browser-based Customizations
Business Connectivity Services x
Business Data Connectivity Service x
Claims-Based Authentication x
Client Object Model (OM)
Configuration Wizards
Connections to Microsoft Office Clients
Connections to Office Communication Server and Exchange n/a
Cross-Browser Support
Developer Dashboard
Discussions
Event Receivers
External Data Column x
External Lists x
High-Availability Architecture n/a
Improved Backup and Restore n/a
Improved Setup and Configuration n/a
Language Integrated Query (LINQ) for SharePoint
Large List Scalability and Management
Managed Accounts n/a
Mobile Connectivity
Multilingual User Interface
Multi-Tenancy n/a
Out-of-the-Box Web Parts
Patch Management n/a
Permissions Management
Photos and Presence
Quota Templates
Read-Only Database Support n/a
Remote Blob Storage (SQL Feature) n/a
REST and ATOM Data Feeds
Ribbon and Dialog Framework
Sandboxed Solutions
SharePoint Designer
SharePoint Health Analyzer n/a
SharePoint Lists
SharePoint Ribbon
SharePoint Service Architecture
SharePoint Timer Jobs x
SharePoint Workspace
Silverlight Web Part
Site Search
Solution Packages
Streamlined Central Administration n/a
Support for Office Web Apps n/a
Unattached Content Database Recovery n/a
Usage Reporting and Logging ?
Visual Studio 2010 SharePoint Developer Tools
Visual Upgrade n/a
Web Parts
Wikis
Windows 7 Support
Windows PowerShell Support n/a
Workflow
Workflow Models
Ask Me About x
Audience Targeting x
Basic Sorting x
Best Bets x
Business Connectivity Services Profile Page x x
Click Through Relevancy x
Colleague Suggestions x
Colleagues Network x
Compliance Everywhere x n/a
Content Organizer x
Document Sets x
Duplicate Detection x
Enterprise Scale Search x x
Enterprise Wikis x
Federated Search x x
Improved Governance x
In-Place Legal Holds x
Keyword Suggestions x
Managed Metadata Service x
Memberships x
Metadata-driven Navigation x
Metadata-driven Refinement x
Mobile Search Experience x
Multistage Disposition x
My Content x
My Newsfeed x
My Profile x
Note Board x
Organization Browser x
People and Expertise Search x
Phonetic and Nickname Search x
Query Suggestions, “Did You Mean?”, and Related Queries x
Ratings x
Recent Activities x
Recently Authored Content x
Relevancy Tuning x
Rich Media Management x
Search Scopes x
Secure Store Service x x
Shared Content Types x
SharePoint 2010 Search Connector Framework x x
Status Updates x
Tag Clouds x
Tag Profiles x
Tags x
Tags and Notes Tool x
Unique Document IDs x
Web Analytics x x
Windows 7 Search x
Word Automation Services x x
Workflow Templates x
Access Services x x
Advanced Content Processing x x x
Advanced Sorting x x x
Business Data Integration with the Office Client x x x
Business Data Web Parts x x x
Business Intelligence Center x x x
Business Intelligence Indexing Connector x x x
Calculated KPIs x x x
Chart Web Parts x x x
Contextual Search x x x
Dashboards x x x
Data Connection Library x x x
Decomposition Tree x x x
Deep Refinement x x x
Excel Services x x
Excel Services and PowerPivot for SharePoint x x x
Extensible Search Platform x x x
Extreme Scale Search x x x
InfoPath Forms x x
PerformancePoint Services x x x
Rich Web Indexing x x x
Similar Results x x x
Thumbnails and Previews x x x
Tunable Relevance with Multiple Rank Profiles x x x
Visio Services x x
Visual Best Bets x x x
External sharing x x x
*Lightweight Public-Facing site x x x
** Office Web Apps x x x

* Items may or may not be offered in the beta timeframe.
** Office WebApps can be installed on Server Std and Enterprise with a License.