Skip Ribbon Commands
Skip to main content

Robin | zevenseas | SharePoint Blog

:

The zevenseas Community > Blogs > Robin | zevenseas | SharePoint Blog > Posts > zevenseas Feature Blocker
January 21
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:

Comments

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
System Account on 27/05/2009 23:58

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
System Account on 29/05/2009 01:31

Rajib

Great Post ,Thanks .
System Account on 11/09/2009 10:06

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?
System Account on 04/11/2009 15:48

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 :)
System Account on 06/11/2009 12:15

UncleJohnsBand

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

Any idea on delivery date?  :-)

Cheers.
System Account on 10/11/2009 05:31

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
System Account on 10/11/2009 20:06

UncleJohnsBand

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

Cheers
System Account on 21/01/2010 19:02

Robin

@UncleJohnsBand, sorry! Yes.. good thing to remind me, will do it very soonish ;)
System Account on 26/01/2010 12:18

UncleJohnsBand

Is it soonish yet??  :-)
System Account on 21/05/2010 16:32
1 - 10Next
 

 Statistics

 
Views: 5524
Comments: 14
Tags:
Published:1582 Days Ago