The Dangers of JavaScript Injection in SharePoint Apps
The latest guidance from Microsoft suggests that the use of client-side API’s is now the preferred technique for customizing the user interface in SharePoint 2013 and SharePoint Online. This is a distinct departure from the tried-and-true approach of using custom master pages to create a branded SharePoint experience. Naturally, this has stirred up quite a debate in the design-oriented segment of the SharePoint community – refer to this excellent blog post on the subject from Cathy Dew, get some additional thoughts from Heather Solomon, and see Chris O’Brien’s developer-oriented take here. Leaving aside for a moment the pros and cons of this approach as it relates to branding, one thing is clear – UI customizations are being pushed quite forcefully back into the hands of developers. This is almost certainly not a good thing, as developers are notoriously poor designers, so it will be imperative that designers and programmers develop a close working relationship in order to ensure that customer requirements are met. Customers, for their part, must be prepared to pay an even higher "customization tax" than ever before, as significant changes will require both branding and development expertise on a single project, or leave aside all requirements for modifying the SharePoint UI (which, for many, will mean just giving up on SharePoint altogether).
To be clear, this doesn’t mean that custom master pages are no longer supported (they are) or that branding is not important (it is) but rather that the pace of change in the cloud product makes the maintenance of custom master pages much more difficult. Remember, SharePoint online is a service not a product – as such it is subject to a much faster update cycle than the on-premise version. To be perfectly frank, if you want complete control over the SharePoint interface, then go with the on-premise product. Your customizations will last longer, breaking changes will be far less frequent and the only time you’ll have to pay the customization "tax" is during the next upgrade cycle. And no, that doesn’t mean you will be stuck with maintaining all those pesky SharePoint servers yourself – there are plenty of vendors willing and able to take that task on for you. But it does mean you will give up online-specific features like Delve in favor of a branded Intranet; whether or not that’s worth the trade-off is a decision each customer will have to make on their own. Based on my experience many customers want both the flexibility of the cloud and the ability to implement some level of interface customizations, which is certainly achievable – within certain limits.
Which leads me to the point of this post – if you decide you want to be in the cloud (or be "cloud ready") but still want some interface customizations, then read on, as there is one big "gotcha" that you absolutely must know about before you move forward.
The technique Microsoft is now recommending for customizations is known as "JavaScript Injection". Simply put, this means pushing client script into a page through the manipulation of existing elements, much as you can do with compiled code and Delegate Controls in an on-premise, full-trust environment. Leveraging the power of client-side frameworks such as jQuery, this technique allows designers to hide, remove, replace, or insert DOM objects as they see fit. There is a good example of this technique in the JavaScript Injection Sample on the Office Developer Patterns and Practices GitHub site. In the sample description, the designer wishes to hide the "new subsite" link on the Site Contents page, which can be accomplished by adding the following JavaScript into the site master page:
$("#createnewsite").parent().hide(); |
In order to "inject" the code programmatically the developer must leverage the Custom Actions functionality of SharePoint within an App. This is the same set of API’s that permit the addition of custom buttons and links to the Ribbon, Menus and Edit Control Block (for more on how to create custom actions refer to the following MSDN Article: https://msdn.microsoft.com/en-us/library/office/jj163954(v=office.15).aspx). There is an existing location identifier in the Custom Action framework named "ScriptLink". It is accessible via the UserCustomActions collection on the parent SPWeb object. By adding a new custom action to the UserCustomActions collection and targeting the ScriptLink element, the code is then inserted into all pages on the site which contain the target element. This can be achieved using the following CSOM code (from the same JavaScript Injection Sample on GitHub referenced above):
public void AddJsLink(ClientContext ctx, Web web) { string scenarioUrl = String.Format("{0}://{1}:{2}/Scripts", this.Request.Url.Scheme, this.Request.Url.DnsSafeHost, this.Request.Url.Port); string revision = Guid.NewGuid().ToString().Replace("-", ""); string jsLink = string.Format("{0}/{1}?rev={2}", scenarioUrl, "scenario1.js", revision);
StringBuilder scripts = new StringBuilder(@" var headID = document.getElementsByTagName(‘head’)[0]; var");
scripts.AppendFormat(@" newScript = document.createElement(‘script’); newScript.type = ‘text/javascript’; newScript.src = ‘{0}’; headID.appendChild(newScript);", jsLink); string scriptBlock = scripts.ToString();
var existingActions = web.UserCustomActions; ctx.Load(existingActions); ctx.ExecuteQuery(); var actions = existingActions.ToArray(); foreach (var action in actions) { if (action.Description == "scenario1" && action.Location == "ScriptLink") { action.DeleteObject(); ctx.ExecuteQuery(); } }
var newAction = existingActions.Add(); newAction.Description = "scenario1"; newAction.Location = "ScriptLink";
newAction.ScriptBlock = scriptBlock; newAction.Update(); ctx.Load(web, s => s.UserCustomActions); ctx.ExecuteQuery(); } |
This is where things start to get interesting. Take note of the fact that the above code is CSOM in a Provider Hosted App – this is a very important point we will be returning to shortly. For the moment, bear in mind that any script inserted programmatically must also be removed programmatically. The following code will reverse the insertion action by removing the script from the UserCustomActions collection:
public void DeleteJsLink(ClientContext ctx, Web web) { var existingActions = web.UserCustomActions; ctx.Load(existingActions); ctx.ExecuteQuery(); var actions = existingActions.ToArray(); foreach (var action in actions) { if (action.Description == "scenario1" && action.Location == "ScriptLink") { action.DeleteObject(); ctx.ExecuteQuery(); } }
} |
It is now time to discuss the "big gotcha" I referenced at the beginning. It stands to reason that if code is injected when an app is added to a site then that same code should be removed when the app is removed. This is an essential element in maintaining a healthy relationship between the developer and the site administrator. If the code is not removed gracefully then it will become orphaned with no way to remove it other than via more code. So the developer must take into account how the functionality they introduced will be retracted when it is no longer required or must be replaced by newer code. There is a very easy way to make sure this happens without requiring any additional effort – use a declarative element to insert the custom action instead of doing it programmatically. The functionality certainly exists within the app model – that’s how Ribbon customizations are performed. Here is an example of how to achieve the same result using a CustomAction element within an App project:
<?xml version="1.0" encoding="utf-8"?> <Elements xmlns="http://schemas.microsoft.com/sharepoint/"> <CustomAction Location="ScriptLink" ScriptBlock="$("#createnewsite").parent().hide();" Sequence="100" /> </Elements> |
Easy, isn’t it? The best part is that declarative elements are automatically removed when the app is removed – no need for code to handle the removal process. So the obvious answer is to deploy a declarative instead of a programmatic custom action, right?
Not so fast.
Try doing this via an App and you will get – nothing. No errors, no deployment failure, no feedback whatsoever. The script is simply never added to the page. This is due to the fact that the parser specifically blocks SharePoint Apps from deploying declarative custom actions containing the "ScriptLink" value for the Location element. So the built-in mechanism for automatically handling the retraction of deployed artifacts is intentionally blocked in an App scenario.
Great. So what now?
Well, if the App in question is a Provider Hosted App, then – in theory – a remote event receiver could be used to call a method that is responsible for deleting the CustomAction element from the host web user actions collection when the app is removed. A remote event receiver is simply a WCF service with an endpoint the App can call in response to an AppInstalled or AppUninstalling event (an example of remote event receivers can be found in the Core.EventReceiversBasedModifications sample). Assuming the service can establish context with the host web and execute the requisite CSOM code then we would have an automated method for script retraction. But wait – is this really a viable solution? Be sure to read the "Dealing with Uninstall" section of the ReadMe page in the sample. It clearly points out that removing an app from the Site Contents page may not fire the AppUninstalling event or may do so with inadequate permissions. So there is no guarantee that a remote event receiver will solve the retraction problem.
Unfortunately, the story only gets worse from here. If the App in question is SharePoint Hosted then there are no options for automatic removal of injected scripts. None. The developer must provide a manual removal method and hope that the site administrator remembers to use it BEFORE removing the app. You read that right – any CustomActions created programmatically in a SharePoint Hosted App cannot be removed except by explicit execution of code. With no ability to add interaction to the app tile in Site Contents there is absolutely no way to notify the user removing an app that additional steps must be taken for graceful removal. The deployed code will remain resident in the master page until additional code is run to remove it.
SharePoint Hosted Apps have many advantages – they are easy to deploy, require no infrastructure on the part of the developer, eliminate the need to write code for establishing and managing context, automatically get provisioned in an app web, and have an integrated navigation experience. The second point is perhaps the most important – developers don’t need to stand up a separate web site just to deploy their app. Sure, spinning up web sites in cloud services is easy and relatively cheap, but it still costs money, especially if a lot of people are hitting it, and not every developer has access to Azure, AWS or Google. As anyone who has ever worked on Enterprise development team can attest it is extremely difficult to get even a simple web site provisioned. Not to mention the hassle and headache of getting S2S trusts set up in on-premise scenarios or opening ports in the firewall for inbound connections for mobile/remote users. SharePoint Hosted Apps are a perfect solution in these scenarios yet they cannot be used for JavaScript injection because there is no mechanism for code retraction.
So where does this leave us in attempting to manage script injection in a responsible manner? Nowhere good, I’m afraid. Declarative actions cannot be used at all because their deployment is blocked. Provider Hosted Apps have an event automation mechanism but it is limited and unreliable. SharePoint Hosted apps have no automation options at all, instead requiring manual code execution without the ability to notify the user. Not to mention the fact that deploying custom actions in both SharePoint and Provider Hosted Apps requires Full Control permissions – a requirement not likely to get past Information Security and explicitly disallowed in apps published to the Office Store. And yet, despite the complete inability to effectively manage the deployment and retraction process, the script injection method via the app model is THE method being promoted by Microsoft for UI customizations in SharePoint 2013 and especially SharePoint Online.
Lest you suffer from the false impression that this is only a customization problem, allow me to correct that misconception. What about developers who rely upon script libraries like jQuery and its multitude of plugins to be present on every page in a site? How about custom style sheets, font libraries, navigation menus, and legal disclaimers? What happens if, for example, you need to invoke a full-page lightbox overlay from an app part? Or pass parameters from an app web page to a host web page via the postMessage API? All of these require script injection and the more of them you have the more orphaned code there will be lurking in the markup of every page.
Is that a big enough "gotcha" to get your attention? I sure hope so. Because if you follow the advice and samples currently being published you will eventually end up with orphaned code in your site(s).
And now for the coup de grâce.
At present, the only viable solution for elegantly managing script injection in all scenarios is…
…are you ready for it?
Sandbox Solutions.
Only script injection performed via a declarative sandbox solution is guaranteed to a) get past the parser in both on-premise and online scenarios, and b) get removed when the solution is deactivated. How is that for coming full circle? The very model for code isolation that was hyped in 2010, then quickly set aside to be replaced by the even more hyped up Cloud App Model in 2013, is the only solution that fully supports the "new" injection method. Of course, Sandbox Solutions come with their own raft of limitations and are considered to be "deprecated" so their use is no longer encouraged. But if you must inject JavaScript into pages in SharePoint then the sandbox is the only safe and reliable method for doing so.
Welcome to the new model. Same as the old model. Only it’s new! And in the cloud!!!
SmartTrack Operational Analytics for SharePoint
What about just injecting the JavaScript via JSOM similar to this: http://www.timferro.com/wordpress/archives/841 (yes i realize i didnt write code to retract and all but imagine I did, lol).
Same problem. You can write the code to inject and remove the script but the site owner must manually execute the removal code. There’s no way to automate that AT ALL. So the likelihood of orphaned actions is almost 100%.
Would it be possible to use JavaScript injection to target the App’s Remove link on the Site Contents page? I don’t think the link has an ID, but if the link can be targeted via jQuery, then you could hijack the execution to perform the script removal before letting SharePoint remove the app itself.
I admit I have not dug into this idea and am sure there is something that prevents this, but if possible, it might work?
and what about inserting a control which removes/inserts javascript when the owner hits the site? Like packaging the rocketignition in a nice box with a button to be pushed by the owner?
Hi Eric, I hope you don’t mind but I took the liberty of posting a link to your blog to the O365 developer group on Yammer to ask for comment. Attached is their response.
Vesa Juvonen (Microsoft)
to O365 Dev Patterns & Practices group on the Office 365 Network Yammer network, February 12 at 1:44am
This is good feedback from Eric and could be indeed an issue in certain scenarios. Our engineering is looking obviously on this feedback and there might be changes on how things are done in the product or in the guidance.
In practice SP hosted apps are designed to modify and work mainly in the app web level and in isolated format, so this would not be an issue. You will have challenges if you do modifications on the host web, but at the same time we need to remember that there’s no automated way to do this with SP hosted apps, so in similar ways as you would have to click something to get the customization to host web, you’d have to click something to get it removed.
Situation is slightly different with provider hosted apps where you can control this with App installed and uninstalled events. Details on this one comes down on what do you actually put on the user custom action in the host web and where is the referenced JS files located… in app web or rather in some CDN, which would be much better option.
One aspect on this discussion is also that these kind of user custom action customizations are often applied to the sites without actually installing any apps on the site… what I mean with this one is that you actually apply the needed configurations to created site collections and sub sites when the site is created (during provisioning) by not adding apps, rather by using remote APIs to set the needed configurations. In that scenario, you do not specifically un-install any apps and therefore you will not face any issues with this. If you use remote provisioning patterns and you apply the needed settings to sites during provisioning, not from installed apps, this won’t hit.
Even with older techniques (farm or sandbox solutions) you had a change to end up having orphan items in the sites, when you retracted your solutions. This means that this is not really a new challenge with the SP, but that’s obviously not valid excuse for repeating same challenge all over again.
There’s always technical details or use cases where things are not optimal (think about many scenarios in feature framework). This is just a nature of evolution of platforms. All this kind of feedback is closely collected and then there are processes to address key concerns from the field.
We do encourage people with feedback and comments on the product level to use User Voice for submitting suggestions. This way we get this feedback under official process and therefore the suggestions are properly tracked.
First, and this is a biggie at the moment, you are downplaying the provider-hosted app issue. Remote event receivers MAY NOT fire when an app is deleted or removed via the UI. Yes, the event handlers are there, but they don’t get executed when they should or potentially get executed with inadequate permissions. This means developers (and site administrators) cannot rely on the event receiver mechanism to remove modifications. So these are really not any better than SharePoint hosted apps. The only solution that works reliably every time is declarative injection via the Sandbox.
Second, the alternative you mention – making modifications directly to sites during programmatic provisioning – is even worse. In that scenario the developer must write an app that enumerates all sites and executes the removal code. You don’t even have an app uninstalling event to try and attach to. Worse, there’s no way other than an enumerator to implement future modifications. This is a really, really bad idea – it would require an entire framework for site provisioning and maintenance. Please, readers, don’t do this (make UI modifications via API’s during site provisioning) unless you are willing to take on the challenge of creating such a framework.
Third, “older techniques” such as full trust code do not, in fact, suffer from this problem. A delegate control can be applied at the Farm level and modified as needed without any orphaned elements, retraction issues, or other such problems. It is an elegant method that, sadly, is lacking in O365. Now, if you mean list templates, instances, and other such artifacts then, yes, that is an issue endemic to the platform but that’s outside the scope of this discussion.
The real problem here is that we (Microsoft and the community) are recommending a process that is highly likely to result in orphaned code elements that cannot easily be removed. If we’re going to push JavaScript injection then we need a much better end to end story; in other words, engineering needs to fix the problems not try to wordsmith around the issue.
Excellent mate.
What if the App in question is a Provider Hosted App ?
Thanks for the help !
Thanks for the really helpful and informative article on javascript injection for sharepoint