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

Search This Blog

Friday, December 20, 2013

How to redirect all but a few users to a custom page in SharePoint 2013

Imagine that you are working in a production environment, you're close to your deadline on which all users will be granted access to your site. But what if you're not ready yet? What if your supervisor wants you to "deny" all users access to the site by redirecting them to one page, all users except for a select few?

In such a case, you'll need a script that will check the name of the user, see if he/she is allowed full access to the site, and if not redirect the user to a custom page. I used Jane Doe and John Doe as the users that are allowed full access to the site (I advise you to add your own name as well).

The code

We need to make a JavaScript file (I named mine "custom-redirect.js") and we will add the following code to the file:
SP.SOD.executeFunc('sp.runtime.js');
SP.SOD.executeOrDelayUntilScriptLoaded('SP.UserProfiles.js', 
 "~sitecollection/Style Library/Scripts/jquery.SPServices-2013.01.js");

var url = window.location.pathname;
if (url.indexOf('Redirect.aspx') > -1) {
 // Do not run the script if we're already on the page.
}
else {
 function redirectMe() {
  var userListArray = new Array();
  userListArray.push("Jane Doe"); 
  userListArray.push("John Doe"); 

  var user = $().SPServices.SPGetCurrentUser({ 
   webURL: "", 
   fieldName: "Title", 
   fieldNames: {}, 
   debug: false
  });
 
  function include(arr, obj) {
   for (var i = 0; i < arr.length; i++) {
    if (arr[i] == obj) return true;
   }
  }

  if(include(userListArray,user)) {}
  else { 
   window.location = 
    "http://your-site-here.com/Pages/Redirect.aspx";
  }
 }
}

The code will check if we're not on the redirect page already, since there's no need to keep running the script when the user has already been redirected. If we're not, then we'll continue with the function called redirectMe().

We need to make an array that will hold the names of all users that will be granted full access to the site. Push the names of those users to the array.

Then, we use SPServices to check the "Title" (also known as the full name of the user) of the current user. We will need a small function to check if an array contains an object as well. Using that function, we then check if the name of the current user is included in the array. If it is, then we don't need to do anything. If the name of the user is not in the array, then we redirect the user to a different page, in my case a custom redirect page.
And that's all there is to it! Quite simple, now that I look back at it.

You'll need to have the jQuery library for SharePoint Web Services in order to get the current user, you can find it here.
Also make sure you added a reference to your script in your master page, like so:
<!--SPM:<SharePoint:ScriptLink language="javascript" ID="scriptLink9" 
 runat="server" OnDemand="false" Localizable="false" 
 name="~sitecollection/Style Library/Scripts/custom-redirect.js" />-->

You'll also need to add the following line of code directly in your master page:
<script type="text/javascript">
 window.onpaint = redirectMe();
</script>

Make sure your master page and your script are checked in. Now you can freely continue working on your site without anyone seeing that it is not yet finished! Better yet, those who you have granted full access to your site can contribute as well!

If you have any questions, feel free to ask. ;)

How to prevent the page from going to "AllItems.aspx" after deleting a list item or a document in SharePoint 2013

Abouth a month ago on the 19th of November, I noticed something while I was cleaning up my site. I discovered that when I deleted a list item or a document from a list or library, the browser would send me to the "AllItems.aspx" of that list or library.

This was something I didn't really pay attention to at first, but then it came to my mind that end users might find this confusing since they would suddenly end up on a different page whenever they deleted something. Off course I understand that you probably wouldn't find this confusing at all, but imagine that a user with no knowledge of anything related to IT or computers would be in that situation. If such a user would end up on a page that no longer has the trusted design with a title and a paragraph of text, and a list or library that is suddenly showing way more columns than what he/she is used to see, that user would probably end up sending me an e-mail asking me for help because he/she is stuck. Yeah.

To avoid any confusion, no matter how small the problem, I wanted to make sure users would never end up on the "AllItems.aspx" page whenever they deleted something. So, not knowing where to start and not finding any relevant results on Google, I asked the question on Stack Exchange. I had thought that something like this wouldn't be so hard to figure out, but was dissappointed when my first and only "answer" was just a comment saying that I could take pointers. After a few weeks with still no solution, I gave up hope and started a bounty on the question.
Suddenly many more answers appeared, yet none were offering me an example in JavaScript. Most answers required me to use C# and seemed way to complicated for a problem this small, and after a while I thought that maybe my question wasn't clear enough.

Until someone answered with three options, one of which was to use cookies. When I started my search, I discovered that there was a successor to cookies. Named HTML5 Web Storage.

The logic behind the idea

First of all, we notice that when you want to delete something (be it a list item or a document, I'll just call it "item" from now on), you first need to click on the edit button for that item before you can choose to delete it. When I edit an item, it opens in a modal dialog. As you may or may not know, the URL of a modal dialog contains "EditForm.aspx".
If I then choose to delete the item, it directs me to the "AllItems.aspx" page. Based on this, we now know that we can write a piece of code that will only run when the user ends up on the "AllItems.aspx" page, having the "EditForm.aspx" page as its referrer.

Next, we just write a piece of code that will take us back to the previous page. But pay attention: technically, the previous page is the modal dialog. We need to go back to the page before we launched the modal dialog, so the page on which you clicked on a list item from a list or a document from a library.

To save the URL of that page somewhere, I used HTML5 Web Storage.
At this point, I just want to share the code with you. After all, there's really not much left to explain.

The actual code

var ref = document.referrer;  // Stores URL of previous page.
var url = window.location.pathname; // Stores URL of current page.

// The following code will run if the user edits a list item or properties of 
// a document.
if (url.indexOf("EditForm.aspx") > -1) {
 sessionStorage.setItem("page", ref);
}

if (url.indexOf("AllItems.aspx") > -1 
    && ref.indexOf("EditForm.aspx") > -1) {
 window.location = sessionStorage.getItem("page");
}

There you have it! It can't be easier, really!
If you follow the code, you'll see that if the URL in your browser window contains "EditForm.aspx", it will store the URL of the "previous" page as a session object. As soon as the URL in your browser window contains "AllItems.aspx" AND if the referrer (aka URL of the previous page) contains "EditForm.aspx", it will set the window location to the URL we stored as a session object earlier. The user won't even see that he/she has been redirected back to that page.

To make things work, you'll need to make sure that you've put a reference to your script in your master page. I had written the code in a new JavaScript file named "no-all-items.js" and added a reference to it in the master page:
<!--SPM:<SharePoint:ScriptLink language="javascript" ID="scriptLink10" 
 runat="server" OnDemand="false" Localizable="false" 
 name="~sitecollection/Style Library/Scripts/no-all-items.js" />-->

And there you have it. Make sure your script is checked in, as well as your master page, and you're ready to go!
Enjoy!

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.