Since I created the Apex Metadata API I get to help all sorts of folks building many different and cool things with it. One that is quite common is to automate post package installation tasks such as updating layouts or picklist values, which are not auto upgraded by the platform.
What makes these use cases a little awkward and less user friendly is that the user installing the package has to perform at least one manual post install step. That is to create the Remote Site Setting to ironically allow the native Apex code to call a native SOAP or REST platform API (see Idea here to remove this need).
What makes it more tricky, is the URL the package install user has to provide for the Remote Site Setting is quite specialised, it has to contain the orgs instance name, the package namespace (if your using the Apex Metadata API from a Visualforce page) and potentially the orgs custom domain name.
As those that follow my blog will know my Declarative Rollup Summary Tool uses the Apex Metadata API and has the same requirement. Since i have defined the Apex Exception User on my package, i get an email notification each time an uncaught exception occurs in the tool. Despite putting instructions on a Welcome page in the tool to setup the Remote Site Setting, i can see by the frequency of System.CalloutException exception i get that this manual post install step is initially being missed.
About the same time i decided to raise an enhancement to remind me to look for a solution or at least better way to indicate this step to the user. I found this question Accessing Metadata API from JavaScript on StackExchange, referencing this earlier one Dynamically set Remote Site in Apex. The infamous Mr Fox had found the answer! Which was to call the Metadata API initially via JavaScript (which is not bound by the Remote Site Setting check). Thankfully since Summer’13 Salesforce supports making API calls from the Visualforce domains, otherwise this would not have been possible due to the browsers cross domain checking! Phew!
I already had configured on the landing tab of the tool a Welcome Visualforce page, so i decided to put the code to auto create the Remote Site Setting here. Note that since Summer’14 you can now also configure a post install page on your package, this would also be a good place. Here is my implementation of the answer given, with a few tweaks, UI and error handling.
You can see that I’ve offered the user two ways, manual and pressing the button. I chose to do this, so that admins would still be aware of what the tool was doing to the configuration in the org, which i feel when using the Metadata API is quite important. Once they press the button it makes the JavaScript callout to the Metadata API and the results are passed back to an apex:actionFunction to refresh the page.
First I wrote some code to test the API connection, which basically made a callout to the Apex Metadata API and caught any exception. I chose to use the listMetadata API call, but it could have been any. Its not the response we are interested in here, it is if it throws an exception or not. Unfortunately there is no language neutral way of checking the lack of Remote Site Setting, so for now the assumption is any CalloutException is a result of this.
global static Boolean checkMetadataAPIConnection() { try { MetadataService.MetadataPort service = new MetadataService.MetadataPort(); service.SessionHeader = new MetadataService.SessionHeader_element(); service.SessionHeader.sessionId = UserInfo.getSessionId(); List<MetadataService.ListMetadataQuery> queries = new List<MetadataService.ListMetadataQuery>(); MetadataService.ListMetadataQuery remoteSites = new MetadataService.ListMetadataQuery(); remoteSites.type_x = 'RemoteSiteSetting'; queries.add(remoteSites); service.listMetadata(queries, 28); } catch (System.CalloutException e) { return false; } return true; }
Next I wrote a Visualforce Controller to call this and also calculate the URL needed by the Remote Site Setting. I did this via the Host HTTP Header, as this will include in a subscriber org all of the above attributes.
public with sharing class WelcomeController { public String Host {get;set;} public String MetadataResponse {get;set;} public Boolean MetadataConnectionWarning {get;set;} public PageReference checkMetadataAPIConnection() { // Get Host Domain Host = ApexPages.currentPage().getHeaders().get('Host'); // Attempt to connect to the Metadata API MetadataConnectionWarning = false; if(!RollupService.checkMetadataAPIConnection()) { // ... See full GitHub code ... MetadataConnectionWarning = true; } return null; } public PageReference displayMetadataResponse() { // ... See full GitHub code ... return null; } }
Finally the Visualforce page with the JavaScript callout to the Metadata API in it! The code constructs the SOAP XML, makes the call and parses the result for any errors. Before calling an apex:actionFunction to refresh the page (only the key parts of the full page are shown below).
<apex:page controller="WelcomeController" tabStyle="LookupRollupSummary__c" standardStylesheets="true" action="{!checkMetadataAPIConnection}"> <script> function createRemoteSite() { // Disable button document.getElementById('createremotesitebtn').disabled = true; // Calls the Metdata API from JavaScript to create the Remote Site Setting to permit Apex callouts var binding = new XMLHttpRequest(); var request = '<?xml version="1.0" encoding="utf-8"?>' + '<env:Envelope xmlns:env="http://schemas.xmlsoap.org/soap/envelope/" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">'+ '<env:Header>' + '<urn:SessionHeader xmlns:urn="http://soap.sforce.com/2006/04/metadata">' + '<urn:sessionId>{!$Api.Session_ID}</urn:sessionId>' + '</urn:SessionHeader>' + '</env:Header>' + '<env:Body>' + '<createMetadata xmlns="http://soap.sforce.com/2006/04/metadata">' + '<metadata xsi:type="RemoteSiteSetting">' + '<fullName>dlrs_mdapi</fullName>' + '<description>Metadata API Remote Site Setting for Declarative Rollup Tool (DLRS)</description>' + '<disableProtocolSecurity>false</disableProtocolSecurity>' + '<isActive>true</isActive>' + '<url>https://{!Host}</url>' + '</metadata>' + '</createMetadata>' + '</env:Body>' + '</env:Envelope>'; binding.open('POST', 'https://{!Host}/services/Soap/m/31.0'); binding.setRequestHeader('SOAPAction','""'); binding.setRequestHeader('Content-Type', 'text/xml'); binding.onreadystatechange = function() { if(this.readyState==4) { var parser = new DOMParser(); var doc = parser.parseFromString(this.response, 'application/xml'); var errors = doc.getElementsByTagName('errors'); var messageText = ''; for(var errorIdx = 0; errorIdx < errors.length; errorIdx++) messageText+= errors.item(errorIdx).getElementsByTagName('message').item(0).innerHTML + '\n'; displayMetadataResponse(messageText); } } binding.send(request); } </script> <body class="homeTab"> <apex:form id="myForm"> <apex:actionFunction name="displayMetadataResponse" action="{!displayMetadataResponse}" rerender="myForm"> <apex:param name="metadataResponse" assignTo="{!metadataResponse}" value="{!metadataResponse}"/> </apex:actionFunction> <apex:sectionHeader title="Declarative Rollups for Lookups" subtitle="Welcome"/> <apex:pageMessages /> <apex:outputPanel rendered="{!MetadataConnectionWarning}"> <input id="createremotesitebtn" type="button" onclick="createRemoteSite();" value="Create Remote Site Setting"/> </apex:outputPanel> </apex:form> </body> </apex:page>
Its worth noting that this solution would also apply to configuring a Remote Site for other types of Salesforce API callouts, such as Apex Tooling API, since its just the Domain part of the URL that needs to be added. Note you may need to add more Remote Sites if your also planning on calling from a Batch Apex context for example.
I’m really pleased with this implementation, but it is a little two baked into the rollup tool at present. This would make a great addition to the Apex Metadata API library (or maybe something more standalone) as a Visualforce Component for example, so that all you need to do is include the component on your welcome or post install pages to use it. Something for another day or a fellow open source developer to think about perhaps!?!
Security Review Notes: This approach has not gone through Security Review as yet. My feeling is that it should pass as there is president for calling Salesforce SOAP/REST API’s from JavaScript already and indeed was the main reason for Salesforce enabling the API endpoints from a VF domain as noted above in Summer’13. Nor does this approach bypass the CRUD, FLS or Permissions needed by such API’s, e.g. you still need to have Author Apex to permit Metadata API calls regardless of where they are called from. Finally the design approach here is user driven (e.g. they have to press abutton) rather than automated on page load (which they do generally discourage of course).