[SalesForce] Sorting Opportunity Line Items programmatically

I've been attempting to do a purely Apex based sorting of Opportunity Line Items based on a solution I found in the Salesforce developer boards. That solution uses a combination of Javascript and Apex WebServices. The Javascript is executed from a custom button and injects a form into the page that mimics the form on the /oppitm/lineitemsort.jsp page and then submits the form.

I'm attempting to do the same but instead of using Javascript I'm trying to do a HTTP POST request from Salesforce-to-Salesforce. Creating a POST request to mimic the form on /oppitm/lineitemsort.jsp then send request. I created a Remote Site Setting to allow for callout to /oppitm/lineitemsort.jsp.

Here's what I have:

public class OppLineSortPost {
    public class CustomException extends Exception {}

    public static void sortLines(Id oppId) {
        HttpRequest req = new HttpRequest();
        req.setHeader('Content-Type', 'application/x-www-form-urlencoded');
        req.setHeader('Cookie', cookie());           
        req.setMethod('POST');
        req.setEndpoint(endpoint());

        String url = '/' + oppId;
        req.setBody(makeBody(new Map<String, String> {
            'id' => oppId,
            'duel0' => sortOrder(oppId),
            'retURL' => url,
            'cancelURL' => url,
            'save' => ' Save ',
            '_CONFIRMATIONTOKEN' => confirmationToken(oppId)
        }));

        Http http = new Http();       
        HttpResponse resp = http.send(req);

        String respString = '\nResponse: ' + resp.toString() + '\n    Body: ' + resp.getBody();

        System.debug(respString);

        if (resp.getStatusCode() != 200) {
            throw new CustomException(respString);
        }
    }

    static String cookie() {
        // When making HTTP Requests we send this Cookie with our session information
        return String.format('oid={0}; sid={1}', new String[] { UserInfo.getOrganizationId(), sessionId() });
    }

    static String endpoint() {
        return String.format(
            'https://{0}/oppitm/lineitemsort.jsp',
            new String[] { System.Url.getSalesforceBaseUrl().getHost() }
        );
    }

    static String sessionId() {
        String sessionId = UserInfo.getSessionId();

        if (sessionId == null) {
            throw new CustomException('No SessionID available');
        }

        return sessionId;
    }

    // Lines will be ordered by Product2.SortOrder__c then by Product2.Name
    static String sortOrder(Id oppId) {
        Id[] sortedIds = new Id[] {};

        for (OpportunityLineItem line : [
            SELECT id
            FROM OpportunityLineItem
            WHERE opportunityId = :oppId
            ORDER BY PricebookEntry.Product2.SortOrder__c , PricebookEntry.Product2.Name
        ]) {
            sortedIds.add(line.id);
        }

        System.debug('ORDER ITEMS: ' + sortedIds);

        return String.join(sortedIds, ',');
    }

    static String confirmationToken(Id oppId) {
        // The Sort form contains a field _CONFIRMATIONTOKEN with a generated code,
        // we make a GET request for form first and scrap the _CONFIRMATIONTOKEN code
        // so we can use it in our subsequent POST request
        HttpRequest req = new HttpRequest();
        req.setHeader('Cookie', cookie());
        req.setMethod('GET');
        req.setEndpoint(String.format('{0}?id={1}&retURL=%2F{2}', new String[] { endpoint(), oppId, oppId }));
        Http http = new Http();
        HttpResponse resp = http.send(req);

        String body = Pattern.compile('[\\s]').matcher(resp.getBody()).replaceAll(' ');
        Matcher m = Pattern.compile('.*?id="_CONFIRMATIONTOKEN" value="(.*?)".*').matcher(body);
        if (!m.matches()) {
            throw new CustomException('Could not find Confirmation Token');
        }

        return m.group(1);
    }


    static String makeBody(Map<String, String> params) {
        if (params.isEmpty()) {
            return '';
        }

        String[] encoded = new String[] {};
        for (String k : params.keySet()) {
            String v = EncodingUtil.urlEncode(params.get(k), 'UTF-8');

            encoded.add(String.format('{0}={1}', new String[] {k, v}));
        }

        return String.join(encoded, '&');
    }
}

So to sort my Opportunity Line Items I would call

OppLineSortPost.sortLines('006e0000002i3HOAAY');

This makes a GET request to to /oppitm/lineitemsort.jsp form first and scraps the _CONFIRMATIONTOKEN code. Then we create a POST request with cookie, _CONFIRMATIONTOKEN and sorted list of Opportunity Line Item ID's. The response I get back is 200 OK but the Opportunity Line Items are not sorted. Seem like the request was just ignored and it wants the redirect the client to the Opportunity page based on the content of the response body.

Here is the Response Body:

<script>
if (window.location.replace){ 
window.location.replace('/006e0000002i3HOAAY');
} else {;
window.location.href ='/006e0000002i3HOAAY';
} 
</script>

<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
<html>
  <head>
    <meta HTTP-EQUIV="PRAGMA" CONTENT="NO-CACHE">
  </head>
</html>

Any one have some ideas on how to get this working? Am I missing some more Headers, Cookies, Form Data?

Best Answer

I gave your code a try and I think I have some more information (no solution yet)

when Salesforce makes a request through a callout it adds proxy headers to the request. I think that those headers are causing the server to respond with a redirect. I have seen the same thing happen when running a reverse proxy with header pass through.

 [HTTP_ACCEPT_LANGUAGE] => en-US,en;q=0.8
 [HTTP_COOKIE] => oid=; sid=; web_core_geoCountry=us; ApexCSIPageCookie=true; rememberUn=true; login=; com.salesforce.LocaleInfo=us; autocomplete=1;  clientSrc=; inst=APPQ;
 -------> [HTTP_SFDC_STACK_DEPTH] => 1         
 [HTTP_ACCEPT_ENCODING] => gzip,deflate,sdch
 [HTTP_USER_AGENT] => Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/28.0.1500.52 Safari/537.36
 [HTTP_ACCEPT] => text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
 [CONTENT_TYPE] => application/x-www-form-urlencoded
 [HTTP_HOST] => 
 -------> [HTTP_VIA] => 1.1 proxy-sjl.net.salesforce.com:8080 (squid)
 -------> [HTTP_X_FORWARDED_FOR] => 10.226.83.3, 10.226.8.155
 [HTTP_CACHE_CONTROL] => max-age=0
 [HTTP_CONNECTION] => keep-alive
 [PATH] => /usr/local/bin:/usr/bin:/bin
 [SERVER_SIGNATURE] => 

since we cant strip those out of the request, a callout is likely a no go. It works from javascript because its being generated from the browser without any of those extra proxy tags.

UPDATE: As I think about it, if you route the request through your own proxy server (ngnix) and strip the tags you might have luck. In this case ugly hack begets uglier hack.

Related Topic