This was a triumph.
I'm making a note here: HUGE SUCCESS.

Search This Blog

Monday, December 2, 2013

How to hide links in the navigation from users that don't have edit permissions for certain pages in SharePoint 2013

Imagine you have a site that has managed navigation. Most of the pages in the site can be seen by all the users, but there is one page that has unique permissions. This page, for example, is named "Secret headquarters" and should only be visible to the members of the group "Secret service". All other users shouldn't even see the link to that page in the navigation, so basically nobody else but the members of the group should know there is such a page.

So, how do we hide a link to a page with custom permissions? And how exactly can we find out in which groups the user is in, and if the user is a member of the group "Secret service"?
Let me explain that to you.

Preparing a page for unique permissions

First of all, you need to create a group that will hold all the users that will have access to the "secret" page. I named my group "Secret service" and added some users. The group has read and edit permissions.
Then, you will need to create the page (if not already) that will be made hidden to all users except those who are a member of the group "Secret service". When you made the page, do the following:
  1. In your subsite, click on the "Settings" button on the top right corner
  2. Click on "Site content"
  3. Click on the name of the "Pages" library (or the "Subsites" library, depending on where you store your pages)
  4. Find the page you want to make secret, and click on the "..." on the right side of its name
  5. In the small modal dialog, click on "..." again and select "Shared with", then click on "Advanced"
  6. In the ribbon, top left icon, click on "Remove Unique Permissions", click "OK"
  7. Select the remaining groups and then click on "Remove User Permissions", click "OK"
  8. Click on "Grant Permissions", type in the name of the group that will have access to the page (in my case, that will be "Secret service")
  9. Click on "Show options" at the bottom of the dialog and untick "Send an email invitation"
  10. Select "Edit" permissions, press "OK"
  11. repeat steps 8 and 9, now select "Read" permissions, press "OK"
At this point, users who are not a member of the group "Secret service" will still see a link to the page in the navigation, but when they click on it, they will get a "Access denied" message.

Testing the permissions of the page

This will be quick and easy to test if you have a dummy account. If not, then I hope you have a colleague willing to spend 10 minutes of his/her time testing your environment. But let's just continue with the idea of having a dummy account.

First, let's add the dummy to the group and see if the dummy can access the page:
  1. With your administrator account, add the dummy account to the group "Secret service"
  2. Log in with the dummy account, navigate to the page "Secret headquarters"
    • If you can see the page with the dummy, then you did well!
    • If you can't see the page with the dummy, you probably didn't add the dummy user to the group "Secret service".
  3. Still on the dummy account, check if the dummy can see other pages in the same subsite
    • If you can still see all other pages with the dummy, then you did well!
    • If you can't see other pages with the dummy, then you probably set the unique permissions for the whole subsite instead of just the one page

Now we just need to check if the dummy will get an "Access denied" when the dummy tries to access the page without being a member of the group:
  1. With your administrator account, remove the dummy account from the group "Secret service"
  2. Log in with the dummy account, navigate to the page "Secret headquarters"
    • If you can't see the page with the dummy, you did well!
    • If you can see the page with the dummy, then you probably didn't remove the dummy from the group "Secret service".
If you passed these small tests, then we are ready to go to the next step!

Writing the code to hide the page from the navigation

Let's first write down what we want to achieve:
  1. Loop through all links in the navigation on the left side of the subsite
  2. When we encounter a list item in which the href attribute ends with "Secret-headquarters.aspx", we want to check the permissions of that page
  3. If we encounter such an element, we will run a function that will fetch all the groups in which the current user is in
    • If the current user is a member of the group "Secret service", we will take no action and leave the navigation as is.
    • If the current user is not a member of the group "Secret service", then we will select that list item holding the link to "Secret-headquarters.aspx" and set it hidden.
I included some comments, be sure to read those too!
// The following three lines are required, don't forget to find a copy 
//of "jquery.SPServices-2013.01.min.js" and add a reference to it here.
SP.SOD.executeFunc("sp.runtime.js");
SP.SOD.executeFunc("SP.js", "SP.ClientContext");
SP.SOD.executeOrDelayUntilScriptLoaded("SP.UserProfiles.js", 
 "~sitecollection/Style Library/Scripts/jquery.SPServices-2013.01.min.js");

var siteUrl = "";
var element = "";

$(document).ready(function() {
 // We only want to loop through the navigation on the left side of the
 // subsite;
 if($("#NavRootAspMenu") != null) {
  // If present, remove the last list item. This sometimes appears and 
  // causes problems since it doesn't have a href attribute.
  $("ul[id*='RootAspMenu'] li.ms-navedit-editArea:last-child").remove();
 }
});

runMe(); 

function runMe() {
 var $this = $("#NavRootAspMenu");
 if($this != null) {   
  $this.find("li").each(function(i){
   // For each list item that has a "a" element, fetch the "href" 
   // attribute and write it to siteUrl.
   siteUrl = $this.find("a.static")[i].href;
   // When the siteUrl ends with "Secret-headquarters.aspx", save the 
   // current element to "element" and run a function.
   if (siteUrl.indexOf("Secret-headquarters.aspx") > -1) {
    element = $this.find("a.static")[i];
    sharePointReady(siteUrl, element);
   }
  });
 }
}          
      
function sharePointReady(siteUrl, element) {
 // Create an array that will hold a list of all the groups where the 
 // current user is a member of.
 var userGroupArray = new Array();
 var group;
 
 // The line below is handy in case you have multiple pages you want to
 // hide, but need to be accessed by different groups. 
 if(siteUrl.indexOf("Secret-headquarters.aspx") >- 1) { 
  group = "Secret service";
 } 
 
 // Get all groups where the current user is a member of.
 var userGroup = $().SPServices({ 
  operation: "GetGroupCollectionFromUser", 
  userLoginName: $().SPServices.SPGetCurrentUser(), 
  async: false, 
  completefunc: function(xData, Status) {
   $(xData.responseXML).find("Group").each(function() {
    // Push the name of the group to the array.
    userGroupArray.push($(this).attr("Name"));
   });
  }
 });
 
 // This useful little function is to check if an element is contained in
 // your array. 
 function include(arr, obj) {
  for (var i = 0; i < arr.length; i++) {
   if (arr[i] == obj) return true;
  }
 }

 // If the array contains the group "Secret service", then do nothing. 
 if(include(userGroupArray,group)) {
  //console.log("You can edit this!");
 }
 // If the array does not contain the group "Secret service", then hide
 // the element from the current user so that he/she cannot navigate to
 // the page. 
 else {
  //console.log("You can't edit this!");
  element.style.display="none";
 }
}

That's it! We're ready with the script. Now it's time to test it out and see if it works.

Adding a reference to the master page

If we want to apply this code on multiple subsites, then it is best that we add a reference to our script in the master page. I just added the code to an existing script that was already loaded on the master page (I use a HTML master page), but if you want to add it as a separate script, this is how it might look like:
<!--SPM:<SharePoint:ScriptLink language="javascript" ID="scriptLink1" 
runat="server" name="~sitecollection/Style Library/Scripts/scripts.js" 
OnDemand="false" Localizable="false"/>-->

Do note that the ID might be different. You must make sure that you do not already have a scriptlink with the same ID, so change the number of the ID and make it unique.
Check in your master page and your script, and go take a look at the page.
You can re-do the steps mentioned in "Testing the permissions of the page", and this time you will immediately see if the list item for the page "Secret headquarters" is present in the list or not.
It should now be hidden from users who are not a member of the group "Secret service", and it will remain visible to those who are a member of that group.

Enjoy!

If you have any questions, please do not hesitate to ask!
Special thanks go to Ali Sharepoint from Stack Exchange, who helped me with the code.
The code for "JavaScript Array Contains" was found on www.css-tricks.com.

Wednesday, November 27, 2013

UPDATE: How to show calendar events in modal dialogs in SharePoint 2013

I recently just noticed that my code for showing events in a calendar in a modal dialog does not always do its job. It appears that when you change to another month and then try to open an event in that month (or in any other month, since it fails the moment you change the month), the event won't open in a modal dialog! This also seemed to be the case when you changed the year.

Since I didn't just want to run the script whenever a user clicked a certain element (like those for the months and for the year), I came up with a different and rather easy fix.
I decided to just run the code everytime the user clicks anywhere inside the #content div. Don't worry, it won't alter any link inside your navigation, only those that have "DispForm.aspx" at the end.
// When called, this function opens the dialog.
function openDialog(pUrl) { 
var options = {
 width : 600,
 height : 400,
 url : pUrl };
 SP.SOD.execute("sp.ui.dialog.js", "SP.UI.ModalDialog.showModalDialog", 
  options);
}

// When called, this function makes sure each event in the calendar is
// opened in a modal dialog instead of on a new page.
function clickMe() {
 $('a[href*="DispForm.aspx"]').each(function() {
  $(this).attr("onclick", "openDialog('" + $(this).attr("href") + "')");
  $(this).attr("href","javascript:void(0)");
 });
}

window.onload = function () {
 clickMe();
 // A timeout is required for when the page first loads (if you would not
 // use a timeout, then the script won't always load).
 setTimeout(function() { 
  clickMe();
 }, 500);
 
 // When you click anywhere inside #content, it will run the function
 // "clickMe()" twice, once with and once without a timeout. 
 $("#content").click(function() {
  clickMe();
  setTimeout(function() {
   clickMe();
  }, 500);
 });
};

Please do note that it might be possible that your div has a different ID. I modified my HTML master page and I believe I added the div with #Content myself (it just wraps around the #sideNavBox and #contentBox divs), but if you're using the Seattle HTML master page, then you'll need to replace the code for the #content div (which I commented in the code below) with the code for #sideNavBox and #contentBox.
 //$("#content").click(function() {
 // clickMe();
 // setTimeout(function() {
 //  clickMe();
 // }, 500);
 //});

 $("#sideNavBox").click(function() {
  clickMe();
  setTimeout(function() {
   clickMe();
  }, 500);
 });
 $("#contentBox").click(function() {
  clickMe();
  setTimeout(function() {
   clickMe();
  }, 500);
 });

Or you could just modify your HTML master page as well and place a wrapping div around #sideNavBox and #contentBox.

Do make sure that you put a reference to this script on the page with the calendar. I mentioned this in my previous post but I'll say it again in case this is the first time you read about this.
I saved my code in a file named "calendar.js", under a folder named "Scripts" in the "Style library" directory.
Edit the page with the calendar, and at the bottom of a page, add a new script editor web part. Add the following code to that web part:
<script src="/Style%20Library/Scripts/calendar.js" 
type="text/javascript"></script>

Now save the page, make sure it is checked in and published, and try it out. If all went well, you'll now be able to open events in a calendar in a modal dialog! ;)

If you have any questions, do not hesitate to ask.

Tuesday, November 19, 2013

How to show calendar events in modal dialogs in SharePoint 2013

!!! UPDATE !!!
I noticed that my code won't always work, especially when you change to another month in the calendar. So I've rewritten it, you can find the new and correct code in this post. 
_________________________

I have a calendar on a page. When you make a new event in the calendar, you get to make it in a default SharePoint modal dialog (I enabled this in the advanced settings of the calendar). However, when you open an existing event in the calendar, it goes to a new page and shows that event as if I never even enabled modal dialogs.
This is some unwanted behavior, what I really want is that any event in the calendar is shown in a modal dialog, just like when you make or edit an event.

So, I decided to write some code. I added comments to the code to explain to you what it does and what it's for.
// When called, this function opens the dialog.
function openDialog(pUrl) { 
var options = {
 width : 600,
 height : 400,
 url : pUrl };
 
 SP.SOD.execute("sp.ui.dialog.js", "SP.UI.ModalDialog.showModalDialog", 
 options);
}

// When the class "ms-acal-month" is loaded, add an onclick attribute to
// all the links ending with "DispForm.aspx" so that the calendar items 
// will open in a dialog instead of on a new page.
$(".ms-acal-month").ready(function () { 
 setTimeout(function modal() {
  $("a[href*='DispForm.aspx']").each(function() {
   $(this).attr("onclick", "openDialog('" + $(this).attr("href") + "')");
   $(this).attr("href","javascript:void(0)"); 
  });
 }, 500);
});

// This function adds an onclick attribute to the class "ms-cal-nav" (the
// a tag that shows/hides extra items), code is needed when there are 
// more than three calendar items on a day. 
$(".ms-acal-month").ready( function() {
 setTimeout(function() {
  $("a.ms-cal-nav").attr("onclick", "clickMe()");
 }, 500);
});

// This function is called when the onclick attribute has been triggered.
// It needs to add the onclick attribute again, since SP automatically 
// removes this attribute as soon as the function was triggered. 
function clickMe() {
 setTimeout(function() {
  $("a.ms-cal-nav").attr('onclick', "clickMe()");
  $("a[href*='DispForm.aspx']").each(function() {
   $(this).attr("onclick", "openDialog('" + $(this).attr("href") + "')");
   $(this).attr("href","javascript:void(0)");
  });
 }, 500);
}

I saved my code in a file named "calendar.js", under a folder named "Scripts" in the "Style library" directory.
In order for the code to do its work, you'll have to put a reference to it on the page with the calendar. Edit the page with the calendar, and at the bottom of a page, add a new script editor web part. Add the following code to that web part:
<script src="//ajax.googleapis.com/ajax/libs/jqueryui/1.9.2/jquery-ui.min.js"
type="text/javascript"></script>
<script type="text/javascript" 
src="~sitecollection/Style%20Library/Scripts/calendar.js"></script>

Now save the page, make sure it is checked in and published, and try it out. If all went well, you'll now be able to open events in a calendar in a modal dialog! ;)

If you have any questions or if you are having problems with the code (not working, errors, unwanted behavior,...) feel free to ask!