After writing the code to block only the publishing feature on the managefeature.aspx page, I was thinking of making it more generic. In such a way that a SharePoint administrator could define in the Central Administration which features are not to be activated or deactivated by the users per web application. So I came up with this:
Lovely eh? ;) So the solution consists of the following items:
- Custom Application Page
- Custom Timer job
- Control Adapter
- .browser file
Custom Application Page
The .aspx page has this in the PlaceHolderMain:
<asp:Content ID="Content7" ContentPlaceHolderID="PlaceHolderMain" runat="server">
<input type="hidden" id="HiddenSiteSelections" runat="server" />
<asp:Label id="JobStatus" runat="server" />
<wssuc:ToolBar id="WebApplicationToolbar" runat="server" CssClass="ms-toolbar">
<template_buttons>
<asp:Label id="uxMessage" runat="server"></asp:Label>
<wssuc:ToolBarButton ID="uxRefreshLink" runat="server" Text="Refresh the page" OnClick="RefreshStatsClick" />
<wssuc:ToolBarButton runat="server"
id="uxInstallBlocker"
Text="Install Blocker"
ToolTip=""
OnClick="InstallBlocker"
ImageUrl="/_layouts/images/newitem.gif"
Padding="2px"
AccessKey="I" />
<wssuc:ToolBarButton runat="server"
id="uxUninstallBlocker"
Text="Uninstall Blocker"
ToolTip=""
OnClick="UninstallBlocker"
ImageUrl="/_layouts/images/newitem.gif"
Padding="2px"
AccessKey="U" />
</template_buttons>
<template_rightbuttons>
<SharePoint:WebApplicationSelector ID="uxWebApplicationSelector" runat="server" />
</template_rightbuttons>
</wssuc:ToolBar>
<wssuc:ToolBar id="Toolbar" runat="server" CssClass="ms-toolbar">
<template_buttons>
<wssuc:ToolBarButton runat="server"
id="Allow"
Text="Unblock"
ToolTip=""
OnClick="AllowLink"
ImageUrl="/_layouts/images/newitem.gif"
Padding="2px"
AccessKey="A" />
<wssuc:ToolBarButton runat="server"
id="Disallow"
Text="Block"
ToolTip=""
OnClick="DisallowLink"
ImageUrl="/_layouts/images/newitem.gif"
Padding="2px"
AccessKey="D" />
</template_buttons>
<template_rightbuttons>
<SharePoint:DVDropDownList id="uxScope" runat="server" AutoPostBack="true" >
<asp:ListItem Text="Farm" Value="Farm"></asp:ListItem>
<asp:ListItem Text="WebApplication" Value="WebApplication"></asp:ListItem>
<asp:ListItem Text="Site" Value="Site"></asp:ListItem>
<asp:ListItem Text="Web" Value="Web"></asp:ListItem>
</SharePoint:DVDropDownList>
</template_rightbuttons>
</wssuc:ToolBar>
<SharePoint:SPGridView ID="gridView" runat="server" AutoGenerateColumns="false" Width="100%" AllowPaging="true" PageSize="50" >
<AlternatingRowStyle CssClass="ms-alternating" />
<Columns>
<asp:BoundField DataField="ID" DataFormatString='<input type="checkbox" group="siteCheckboxes" name="{0}" onclick="checkBoxClick();">' HtmlEncode="false" HeaderText='<input type="checkbox" id="selectAllCheckBox" onclick="selectAllCheckBoxClick(this);" />' ItemStyle-Width="1"/>
<SharePoint:SPBoundField DataField="Title" HeaderText="Title" HeaderStyle-Font-Bold="true" />
<SharePoint:SPBoundField DataField="Description" HeaderText="Description" HeaderStyle-Font-Bold="true" />
<SharePoint:SPBoundField DataField="Disallowed" HeaderText="Blocked" HeaderStyle-Font-Bold="true" />
</Columns>
</SharePoint:SPGridView>
</asp:Content>
So no HTML at all, just SharePoint and ASP.NET Controls in there.
Now I’m not going to place the entire codebehind file here, just the important things I think are interesting enough to blog about here :)
- Getting all the installed features on the farm
foreach (SPFeatureDefinition featureDefinition in SPFarm.Local.FeatureDefinitions)
{
}
- Storing all the selected features in a persistent object that sits in the Web application so that the controladapter can access it while rendering.
If you are asking yourself why am I storing this in a persistent object in the Web application and not somewhere in a list or the propertybag of the Central Admin. It’s because of the following reason: If you are running with elevated priviliges in a Web application, the account you are impersonating is the application pool account. Now by default this account has no permissions to look into other Web applications (like the Central Admin) but it does have permissions to get persistent objects from his own Web application. And next to that, if you have access to the central admin then (most of the time) you are also farm admin, meaning that you can set persistent objects into other Web applications.
SPWebApplication webApplication = uxWebApplicationSelector.CurrentItem;
FeatureCollection featureCollection = webApplication.GetChild<FeatureCollection>("FeatureCollection");
List<string> features = new List<string>();
// If no settings previously created, create them now.
if (featureCollection == null)
{
SPPersistedObject parent = webApplication;
featureCollection = new FeatureCollection("FeatureCollection", parent, Guid.NewGuid());
featureCollection.Update();
}
string[] ids = base.Request["ctl00$PlaceHolderMain$HiddenSiteSelections"].ToString().Split(new Char[] { '#' });
foreach (string id in ids)
{
if (!string.IsNullOrEmpty(id))
{
features.Add(id);
}
}
featureCollection.Features = features;
featureCollection.Update();
- Installing the timerjob to run immediately that ensures that the assembly and the .browser file gets copied into the Web application folder
SPWebApplication webApplication = uxWebApplicationSelector.CurrentItem;
if (webApplication.JobDefinitions.GetValue<FeatureBlockerTimerJob>("FeatureBlockerTimerJob") == null)
{
FeatureBlockerTimerJob installFeatureJob = new FeatureBlockerTimerJob("FeatureBlockerTimerJob", webApplication, null, SPJobLockType.Job);
SPOneTimeSchedule schedule = new SPOneTimeSchedule(DateTime.Now);
installFeatureJob.Schedule = schedule;
installFeatureJob.Name = "FeatureBlockerTimerJob";
installFeatureJob.Title = "FeatureBlockerTimerJob";
installFeatureJob.Installing = true;
installFeatureJob.Update();
SetWebProperty("true", "Installed" + webApplication.Id.ToString(), SPContext.Current.Web.Properties);
}
On to the timer job!
Custom Timer Job
In the Execute method we do the following to get the .browser file and the assembly into the webapplication folder. Now I think you are questioning yourself.. why not put the assembly in there while installing the solution and only copy the .browser file in the App_Browser folder. Well.. there is one good reason for that and that is: if you only copy the .browser file in there, SharePoint won’t notice that something has changed in the Web application folder. So to make SharePoint notice that something has been changed, I’ve put the .dll in the bin folder because that will trigger it to check the Web application folder again. Even an IISRESET does not do the trick to ensure that it will check for a new .browser file (although I haven’t tested this thoroughly).
SPIisSettings iisSettings = this.WebApplication.GetIisSettingsWithFallback(SPUrlZone.Default);
//Browser file
//Setting the destination folder
string destinationPath = String.Format("{0}\\{1}\\{2}", iisSettings.Path, appBrowserPath, browserFile);
//Getting the folder where the custom .browser file is
string filePath = String.Format("{0}\\FEATURES\\{1}\\{2}", SPUtility.GetGenericSetupPath("Template"), featureName, browserFile);
FileInfo fi = new FileInfo(filePath);
try
{
fi.CopyTo(destinationPath);
}
catch (Exception) { }
//Assembly
filePath = String.Format("{0}\\FEATURES\\{1}\\{2}", SPUtility.GetGenericSetupPath("Template"), featureName, assemblyName);
fi = new FileInfo(filePath);
DirectoryInfo[] binFolder = iisSettings.Path.GetDirectories("bin");
if (binFolder.Length == 0)
{
iisSettings.Path.CreateSubdirectory("bin");
}
destinationPath = String.Format("{0}\\{1}\\{2}", iisSettings.Path, binPath, assemblyName);
try
{
fi.CopyTo(destinationPath);
}
catch (Exception)
{ }
Control Adapter
And we do the same for removing the assembly and the .browser file. Next thing is the control adapter that will check if there is any persistent object to be found in the Web application. If it’s found then it’s going to append a script line to disable the button of that particular feature.
public class FeatureBlockerAdapter : ControlAdapter
{
private List<string> featureCollection;
protected override void Render(System.Web.UI.HtmlTextWriter writer)
{
SPSecurity.RunWithElevatedPrivileges(delegate()
{
SPWebApplication webApplication = SPWebApplication.Lookup(SPContext.Current.Site.WebApplication.GetResponseUri(SPUrlZone.Default));
FeatureCollection settings = webApplication.GetChild<FeatureCollection>("FeatureCollection");
if (settings != null)
{
featureCollection = settings.Features;
}
});
StringBuilder sb = new StringBuilder();
HtmlTextWriter htw = new HtmlTextWriter(new StringWriter(sb));
base.Render(htw);
if (featureCollection.Count > 0)
{
foreach (string feature in featureCollection)
{
string output = feature;
if (feature.StartsWith("{"))
{
output = output.Replace("{", "");
}
if (feature.EndsWith("}"))
{
output = output.Replace("}", "");
}
sb = sb.Append("<script>var siteButton = document.getElementById('"+output+"').childNodes[0];siteButton.disabled = true;</script>");
}
}
writer.Write(sb.ToString());
}
}
.Browser file
<browsers>
<browser refID="Default">
<controlAdapters>
<adapter controlType="Microsoft.SharePoint.WebControls.FeatureActivator,
Microsoft.SharePoint.ApplicationPages, Version=12.0.0.0, Culture=neutral, PublicKeyToken=71e9bce111e9429c"
adapterType="zevenseas.SharePoint.Solutions.FeatureBlocker.FeatureBlockerAdapter" />
</controlAdapters>
</browser>
</browsers>
So as always.. this is it. If you want take a look for yourself how I did the packaging and created the whole project just grab it here from Codeplex and let me know what you think of it ;)
Technorati Tags:
SharePoint