zevenseas


 

zevenseas Feature Blocker

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='&lt;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:

Links to this post

Comments

On 28 May 2009 08:58, Damian

Hi Robin,

very nice and helpful post! But i wasn't able to download the mentioned solution, neither from this page nor from CodePlex.

Cheers
Damian

On 29 May 2009 10:31, Robin

Hi Damian,

that's weird.. I've got an update that I want to publish soon anyway. If you want I can email you the solution as well.

Robin

On 11 Sep 2009 07:06, Rajib

Great Post ,Thanks .

On 05 Nov 2009 12:48, UncleJohnsBand

Hi Robin....I posted this over at codeplex as well in the issues area but figured I would try posting here as well....

Perhaps this is how this is to work.....but it seems odd.

If I block Enterprise Services at the web level then switch to the site level and block Enterprise Services there.....the settings for what was blocked on the Web level are lost. Can you only block items at one level or is this a bug of some sort?

On 06 Nov 2009 09:15, Robin

Hi,

yes, you are correct. In this version you can only block a certain scope (farm/webapp/site/web) features. On the other question, the enterprise features appear both in the web and sitecollection features page. If you block the sitecollection one, it will be sufficient because the webfeature is dependent on that sitecollection feature.

I will upload a modification to the featureblocker tool to support multiple scoped features :)

On 10 Nov 2009 02:31, UncleJohnsBand

Excellent Robin! Looking forward to the multi-scope feature update.

Any idea on delivery date? :-)

Cheers.

On 11 Nov 2009 05:06, UncleJohnsBand

I didn't realize that you could only pick one scope period to block....I originally thought the issue was just if the feature itself was scoped on two different levels (i.e. Enterprise Services).

Will your update allow the blocking of features at different levels at the same time (regardless of them being multi-scoped features or not).

For example....I want to disable:

* The Excel Services at the farm level
* Office SharePoint Server Web Application features at the Application Level
* Office SharePoint Server Enterprise Site Collection features at the Site level
* Translation Management Library feature at the Web level
* Office SharePoint Server Enterprise Site features at the Site level

All at the same time.....

That's all.... :-)

Cheers

On 22 Jan 2010 04:02, UncleJohnsBand

Hi Robin.....I thought I would check back on any updates on your solution.

Cheers

On 26 Jan 2010 09:18, Robin

@UncleJohnsBand, sorry! Yes.. good thing to remind me, will do it very soonish ;)

Name

Url

Email

Comments

CAPTCHA Image Validation



 
 
 

© 2009 Community Kit For SharePoint