<Update>Check out this later post zevenseas Feature Blocker which basically is an update of this post. Or go to CodePlex and download the solution immediatly ;) </Update>
If you want to prevent users from activating certain out-of-the-box features like the Publishing feature on a non-publishing site there isn’t really much you can do about it. When I was solving this problem the first thing that sprung into mind was to attach an event handler on the feature activating method but unfortunately there is no event handler available to do this (MS if you read this.. this is on my O14 wish list as well from now on :)
Next thing that sprung into mind was to write a bit of javascript that would disable the button. Why? Well in the rendered html of the managefeatures page every feature is written down like this:
<TR><TD class="ms-vb2" style="font-weight: bold;"><H3 class="ms-standardheader">Office SharePoint Server Publishing Infrastructure</H3></TD></TR>
<TR><TD class="ms-vb2">Provides centralized libraries, content types, master pages and page layouts and enables page scheduling and other publishing functionality for a site collection.</TD></TR>
</TABLE>
</TD>
<TD class="ms-alternating" style="padding-top: 4px; padding-bottom: 4px;">
<DIV id='f6924d36-2fa8-4f0b-b16d-06b7250180fa'>
<input type="button" name="ctl00$PlaceHolderMain$featact$rptrFeatureList$ctl33$btnActivate" value="Activate"
onclick="javascript:__doPostBack('ctl00$PlaceHolderMain$featact$rptrFeatureList$ctl33$btnActivate','')"
id="ctl00_PlaceHolderMain_featact_rptrFeatureList_ctl33_btnActivate" class="ms-ButtonHeightWidth" />
</DIV>
</TD>
So with a piece of nifty javascript we can get the DIVbased on the GUID of the feature. And if we have the DIV, we can also get the assoicated input button and set it to disabled using this:
var siteButton = document.getElementById('f6924d36-2fa8-4f0b-b16d-06b7250180fa').childNodes[0];
siteButton.disabled = true;
The question is how to embed that piece of javascript!
So I had a little discussion with some of my SharePoint friends namely Daniel and Waldek (who now is MVP!) and got two suggestions:
- Write a HTTPModule
- A good example of writing a HTTPModule which does what I want can be found on Codeplex and is SharePoint ActiveX Override (SPAXO). What this thing does is, it adds a bit of javascript that takes care of hiding the Name.dll ActiveX warning. With a little tweaking I got it working like the way I wanted it to work but the nasty habit of HTTPModules is that they intercept every page you are requesting. Considering the fact that I only want to have inject my piece of javascript on the ManageFeatures page I found this approach costing too much performance for the simple thing I wanted it to do.
- Write a ControlAdapter
- Next it’s time to let SharePoint know that you have created an adapter by creating a .browser file that sits in the App_Browsers folder. First you specify which control you are trying to override and secondly you specify which control must be used for the overriding. It looks something like this::
<browsers>
<browser refID="Default">
<controlAdapters>
<adapter controlType="Microsoft.SharePoint.WebControls.FeatureActivator, Microsoft.SharePoint.ApplicationPages,
Version=12.0.0.0, Culture=neutral, PublicKeyToken=71e9bce111e9429c"
adapterType="PreventPublishing.FeatureActivatorAdapter" />
</controlAdapters>
</browser>
</browsers>
- You may have never guessed but this stuff actually works! And now the tricky part.. making this deployable! ;) I’ve never had to put anything in the folder of a WebApplication folder so I was checking all the properties and methods of the WebApplication class and then I found the SPIisSettings class! Time for a new paragraph..
SPIiSettings
This class gives you access to the WebApplication folder of the specified UrlZone (Default/Intranet/Extranet/etc). If you take a look at the following code (that could do a little more polishing, I know..) that is implemented in a web application scoped feature receiver I get a reference to the App_Browsers folder to store the .browser file in. By using the Properties element in the Feature.xml I also get a reference to the .browser file that resides in the feature folder. So by combining the two paths I can use a StreamReader and a StreamWriter to copy the file across.
SPWebApplication webApplication = (SPWebApplication) properties.Feature.Parent;
SPIisSettings iisSettings = webApplication.GetIisSettingsWithFallback(SPUrlZone.Default);
//Setting the destination folder
string destinationPath = iisSettings.Path + @"\App_Browsers\preventpublishing.browser";
FileInfo fi = new FileInfo(destinationPath);
//Getting the folder where the custom .browser file is
string filePath = properties.Feature.Properties["Path"].Value;
using (StreamReader sr = new StreamReader(filePath))
{
using (StreamWriter sw = fi.CreateText())
{
string line;
// Read and display lines from the file until the end of
// the file is reached.
while ((line = sr.ReadLine()) != null)
{
sw.WriteLine(line);
}
}
}
And that’s pretty much it! :) After all this.. this is how the end-user sees the page:
Some very useful references that helped me :