Freitag, 22. August 2014

Check if CurrentUser belongs to a SharpeointGroup due to AD-Groups

Recently I had to check if a User belongs to specific SharePoint-Groups. While SPUser.Groups worked well as long as the user was directyl added to the Groups, it did not work when an Active-Directory Group that the user belongs to was added to a SharePoint-Group.

I first tried by resolving the AD-Groups iteratively, however that was obviously quite slow and expensive. There had to be a better solution and I found it:

As SharePoint uses Claims-Authentication (by Default in 2013) all the AD-Groups (at least their SIDs) are alredy in the users Claims, including their complete hirarchy.

So all you have to do is something like:

var claims = ((Microsoft.IdentityModel.Claims.ClaimsIdentity) testuser.Identity).Claims;
to get the current Users Claims. In this Claims collection you have besides others the current users Groups, represented by their SIDs. You have to reference Microsoft.IdentityModel, which is usually located in GAC on a SP2013 Server.

To check if the user or one of it´s Groups belongs to a specific SharePoint-Group, you simply have to do:
SPSecurity.RunWithElevatedPrivileges(delegate{
   using (var elevatedSite = new SPSite(SPContext.Current.Site.ID))
{
   var web = elevatedSite.RootWeb;
var group = web.SiteGroups.GetByName(groupName);
foreach (SPUser user in group.Users)
{
   if (user.IsDomainGroup)
{
   //var users = new List();
var sid = user.LoginName.Split('|')[1];
if (claims.Any(c => String.Equals(c.Value, sid, StringComparison.CurrentCultureIgnoreCase)))
{
   returnValue = true;
break;
}
} 
   else if (user.LoginName == userName)
{
returnValue = true;
break;
}
}
}
});

Maybe there is a simple way to get all SharePoint-Groups, where one of the SIDs belong to, too. I did not try that yet.

Show only 5 Days in Default-Calendar View (Mo-Fr)

I just had the requirement to Show only Monday to Friday in the Calendar View. The Environment was SharePoint 2010, I only had rights to edit the User Interface.

So I added a CEWP, which is linked to a Textfile located in a doc-Library, containing the following Script. The Script is overwriting some Default-Functions, which are responsible for Rendering. Besides some specific Appointments, starting with a specified String are rendered with additional attributes and it is showing all appointments (no expand/collapse anymore)

The Script is using some functions of (an old verions) of JQuery, because it is already loaded on the site. However, it might not be very difficult, to replace that functions into a native-way, if you don´t have JQuery.

The Content of my script-File:

<script language="javascript">
ExecuteOrDelayUntilScriptLoaded(function() {
//Removing Weekend and adjusting the calculated width of appointments
$('th.ms-acal-week-top').css('width', '20%');
$('th.ms-acal-month-top').css('width', '20%');
$('table.ms-acal-month tr').each(function(index) {$(this).find('td:gt(4), th:gt(5)').remove()});
SP.UI.ApplicationPages.CalendarTableBase.prototype.$3T = function(c,d){
        ULSvSp:;
        for(var b=0,a=0;
        a<d && a < 5;
        a++)b+=this.$1o(c+a);
        return b;
    }

SP.UI.ApplicationPages.CalendarTableBase.prototype.get_$2f = function(){
        ULSvSp:;
        return 5;
};

SP.UI.ApplicationPages.CalendarStateHandler.prototype.onTableSucceed =function(a){
        ULSvSp:;
        this.$D_1.$4V();
        this.$3H_1=false;
        a.Options&2&&this.$D_1.$7s(a.Table);
 $('table.ms-acal-detail tr').each(function(index) {$(this).find('td:gt(4), th:gt(5)').remove()});
        a.Options&8&&a.Options&32&&this.$D_1.$86(a.Dates,a.RangeJDay);
        this.$D_1.$7Z(this.$a_1.get_$2C());
        this.$2c_1=false;
        0===(a.Options&1)&&this.$2A_1()
};
//set all days to "Expanded"
SP.UI.ApplicationPages.CalendarInfoGrid.prototype.$38_0 = true;
//Applying specific background-color for all appointments starting with "Absenz"
SP.UI.ApplicationPages.ItemBuilder.prototype.$4P = function($p0, $p1, $p2, $p3, $p4, $p5, $p6, $p7) {
        var $v_0 = $p1.$c;
        $p0.append('<div class=\"ms-acal-item');
        if (!$v_0.$N.primary) {
            $p0.append(' ms-acal-color');
            $p0.append($v_0.$N.color);
        }
        $p0.append('\" style=\"');
        this.$3j_0($p0, $p2, $p3, $p4, $p5);
        $p0.append('border-width:');
        $p0.append($p6);
 if ($v_0.$3R.match(/^absenz/i))
 {
  $p0.append(';background-color: pink');
 } 
        $p0.append(';\" title=\"');
        this.$4X_0($p0, $v_0);
        $p0.append('\"');
        this.$4D_0($p0, $p1.$2K);
        $p0.append('\">');
        $p0.append('<div class=\"ms-acal-mdiv\">');
        if ($p7) {
            this.$5Q_0($p0, $v_0);
        }
        this.$3N_0($p0, $v_0);
        if (!this.$5_0($v_0.$1G)) {
            $p0.append(' (');
            $p0.append($v_0.$1G);
            $p0.append(')');
        }
        $p0.append('</div></div>');
    }
}, "SP.UI.ApplicationPages.Calendar.js");
</script>

Don´t really know, if it works in SP 2013 too, but as SP2013 is doing the rendering also in JS, it might be not very different to the SP2010 solution

Regards

Montag, 24. Februar 2014

Custom Autocomplete for Searchbox using Search Results

Lately I got an request, to give the search Box an "Autocomplete-Feature". OOTB there is already such a Feature, but it works only with fixed and via Central-Administration uploaded Suggestions or the Users personal "favorites" (Queries, that the user did at least 6 times).

The request was, that the SearchBox should Show available words for Searching, matching the users current entry.

CustomSearchBox.AutoCompleteBehavior.prototype = Object.create(AjaxControlToolkit.AutoCompleteBehavior.prototype); CustomSearchBox.AutoCompleteBehavior.prototype._getSuggestion = function () { try { var clientContext = new SP.ClientContext.get_current(); var keywordQuery = new Microsoft.SharePoint.Client.Search.Query.KeywordQuery(clientContext); So, here is what I did:

The search Box uses the "AjaxControlToolkit.AutoCompleteBehavior", so this is where I started to hook in.

First, I made a custom Class, which basically is a copy of AjaxControlToolkit.AutoCompleteBehavior", and overwrite the method, which is loading the displayed Suggestions: 
Type.registerNamespace('CustomSearchBox');

function initBehavior() {
    CustomSearchBox.AutoCompleteBehavior = function (element) {
        AjaxControlToolkit.AutoCompleteBehavior.initializeBase(this, [element]);
    };

            keywordQuery.set_queryText("*" + this._currentPrefix + "*");
            var searchExecutor = new Microsoft.SharePoint.Client.Search.Query.SearchExecutor(clientContext);
            var results = searchExecutor.executeQuery(keywordQuery);
            clientContext.executeQueryAsync(Function.createDelegate(this,
                function (sender, e) {
                    try {
                        var rows = results.m_value.ResultTables[0].ResultRows;
                        var reg = new RegExp("\\b\\w*" + this._currentPrefix + "\\w*\\b","ig");
                        var regPart = new RegExp("(" + this._currentPrefix + ")", "ig");
                        var words = [];
                        
                        for (var i = 0; i < rows.length; i++) {
                            var row = rows[i];
                            var hhp = row.HitHighlightedSummary.replace(/<(.|\n)*?>/g, '');

                            var match = reg.exec(hhp);
                            if (match) {
                                for (var j = 0; j < match.length; j++) {
                                    {
                                        var bolded = match[j].replace(regPart, "$1");
                                        if (words.indexOf(bolded) == -1)
                                            words.push(bolded);
                                    }
                                }
                            }
                        }

                        var completionItems = new Microsoft.SharePoint.Client.Search.Query.QuerySuggestionResults();

                        completionItems.set_queries(words);
                        completionItems.set_peopleNames([]);
                        completionItems.set_personalResults([]);

                        this._update(this._currentPrefix, completionItems, true);
                    }
                    catch (ex) {
                        Srch.U.trace(null, "CustomSearchBox.AutoCompleteBehavior._update", ex.toString());
                    }
                }),
                function (sender, failureArgs) { });
        }
        catch (e) {
            Srch.U.trace(null, "CustomSearchBox.AutoCompleteBehavior._getSuggestion", e.toString());
        }
        $common.updateFormToRefreshATDeviceBuffer();
    };
    
    CustomSearchBox.AutoCompleteBehavior.registerClass('CustomSearchBox.AutoCompleteBehavior', Sys.UI.Behavior);
    CustomSearchBox.AutoCompleteBehavior.descriptor = AjaxControlToolkit.AutoCompleteBehavior.descriptor;
}

function activateCustomSuggestionBehavior(control) {
    if (control.$3G_3) {
        SP.SOD.executeFunc('sp.js');
        EnsureScriptFunc('ajaxtoolkit.js', 'AjaxControlToolkit.AutoCompleteBehavior', function() {
            if (Srch.U.n(control.$f_3[control.$I_3])) {
                initBehavior();
                var $v_0 = {};
                $v_0['id'] = control.$I_3;
                $v_0['completionInterval'] = control.$2y_3;
                $v_0['completionListElementID'] = control.$I_3;
                $v_0['parentElementID'] = control.$c_3;
                $v_0['completionListCssClass'] = 'ms-qSuggest-list';
                $v_0['completionListItemCssClass'] = 'ms-qSuggest-listItem';
                $v_0['personalResultTitleCssClass'] = 'ms-qSuggest-personalResultTitle';
                $v_0['hrCSSClass'] = 'ms-qSuggest-listSeparator';
                $v_0['enterKeyDownScript'] = '$find(\'' + SP.Utilities.HttpUtility.ecmaScriptStringLiteralEncode(control.get_id()) + '\').search($get(\'' + SP.Utilities.HttpUtility.ecmaScriptStringLiteralEncode(control.$d_3) + '\').value)';
                $v_0['highlightedItemCssClass'] = 'ms-qSuggest-hListItem';
                $v_0['minimumPrefixLength'] = control.$30_3;
                $v_0['offsetWidth'] = -2;
                $v_0['queryCount'] = control.$2z_3;
                $v_0['personalResultCount'] = control.$2w_3;
                $v_0['showPeopleNameSuggestions'] = control.$3F_3;
                $v_0['personalResultTitleSingular'] = Srch.Res.qs_PersonalResultTitleSingular;
                $v_0['personalResultTitlePlural'] = Srch.Res.qs_PersonalResultTitlePlural;
                $v_0['overlappingElementID'] = control.$F_3;
                $v_0['sourceId'] = control.get_querySuggestionsSourceID();
                Srch.U.createBehavior(control.$I_3, CustomSearchBox.AutoCompleteBehavior, $v_0, control.$d_3);
                control.$f_3[control.$I_3] = true;
            }
        });
    }
}
Then I created a custom DisplayTemplate which includes the scriptfile with the above js-code:

    <script>

        $includeScript(this.url, "~sitecollection/Style Library/Custom/JSLibraries/Custom.Search.js");

    </script>


and change the onkeydown function of the corresponding input

  <input type="text" value="_#= $htmlEncode(ctx.ClientControl.get_currentTerm()) =#_" maxlength="2048"

    accesskey="_#= $htmlEncode(Srch.Res.sb_AccessKey) =#_"

    title="_#= $htmlEncode(prompt) =#_"

    id="_#= $htmlEncode(searchBoxId) =#_" autocomplete="off" autocorrect="off"

    onkeypress="if (Srch.U.isEnterKey(String.fromCharCode(event.keyCode))) { $getClientControl(this).search(this.value);return Srch.U.cancelEvent(event); }"

    onkeydown="var ctl = $getClientControl(this); activateCustomSuggestionBehavior(ctl);"

    onfocus="var ctl = $getClientControl(this);ctl.hidePrompt();ctl.setBorder(true);"

    onblur="var ctl = $getClientControl(this);ctl.showPrompt();ctl.setBorder(false);"

    class="_#= inputClass =#_" />


Last but not least I had to assign the new Displaytemplate to the SearchBox, and I had a Autocomplete-Suggestion function using the search.

Anyway, as the sharepoint search is used, you have to keep following in mind:
- Search delivers and orders results based on the current user, so it is possible that the autocomplete delivers different results for different users
- Search delivers results in pages (default 10 items per page), so in the above version, if all of the items on the first page contains the same word, you might get only one word in the autocomplete box. You can change this by changing the pagesize in the query issued by the autocomplete-suggestion function. But keep in mind: the more items you load, the greater is the impact on performance
- If it doesn´t work check the following:
 -- necessary SP-js-files are loaded
 -- ShowPeopleSuggestions is activated in the search-box Webparts properties

Of course you could also do that by putting the whole JS-Code in a Content-Editor-Webpart and overwrite the onkeydown-function there. However, in my opinion the above is the cleanest and most modular solution.

Regards