10

Some of my users cannot view the Case object owing to restricted licenses. To avoid paying for service cloud licenses, I need to generate PDF snapshots of all Case records using a batch (the are many case records which unfortunately necessitates an async approach)

After encountering blank redirect pages from getContent(), the apex docs explained my problem:

This method can't be used in:

  • Triggers
  • Scheduled Apex
  • Batch jobs
  • Test methods
  • Apex email services

http://www.salesforce.com/us/developer/docs/apexcode/Content/apex_System_PageReference_getContent.htm

So after some research I almost got it working by installing a package "batchpdf" - but I am struggling to get the native printable view to render even though it works ok for visualforce pages.

Here is the the running code - it doesn't throw any errors but the PDF is blank unless I use a visualforce page:

public class CasePrinter implements Database.Batchable<SObject>, Database.AllowsCallouts
{
    private String m_sessionid;
    public CasePrinter( string sessionid ) 
    {
        m_sessionid = sessionid;
    }

    public Database.QueryLocator start(Database.BatchableContext cTx) 
    {
        return Database.getQueryLocator('SELECT Id FROM Case LIMIT 10');
    }

    public void execute(Database.BatchableContext ctx, List<Case> scope) 
    {
        Case c = scope[0];
        string path = '/' + c.id + '/p';
        BatchPDF.PageReference pr = new BatchPDF.PageReference( path );

        // insert
        Attachment a =  new Attachment();
        a.Name       = 'snapshot' + c.id + '.pdf';
        a.Body       = pr.getContentAsPDF( m_sessionid );
        a.Parentid   = c.id;
        insert a;
    }

    public void finish(Database.BatchableContext ctx) {}
}

And this is how it gets invoked:

Database.executeBatch(new CasePrinter(UserInfo.getSessionId()), 1);

but I get an empty PDF.

  • I have checked the source of the page and it looks like a redirect to the login page
  • I have looked at ApexPages.StandardController but there is no print() method
  • I also tried creating a visualforce page with an iframe which loads the printable view eg https://na1.salesforce.com/500d0000004Ixt5/p.

Unfortunately, none of these approaches work. Any and all assistance is much appreciated.

package link https://login.salesforce.com/packaging/installPackage.apexp?p0=04tb00000009xcG

  • 1
    just pay for the licenses man... how many users do you have? – bigassforce Feb 28 '14 at 13:38
  • 3
    Tend to agree with @user320 or create a view only clone of cases in a custom object and give your users access to that. – Daniel Hoechst Feb 28 '14 at 14:05
  • 1
    If you dont mind sitting at a visualforce page you can spin up your own batch job implementation that is not subject to getContent limitations with either an apex:actionPoller or javascript's setInterval method. – Phil Rymek Feb 28 '14 at 19:35
  • 1
    @max where's the package? – bigassforce Mar 3 '14 at 11:55
  • @user320 see edited post – max Mar 3 '14 at 12:03
11
+100

Change your Batch Apex class from this:

string path = '/' + c.id + '/p';
BatchPDF.PageReference pr = new BatchPDF.PageReference( path );

to this:

string path = Page.CaseDetail.getUrl() + '?id=' + c.Id;
BatchPDF.PageReference pr = new BatchPDF.PageReference( path );

So instead of using the printable view URL like/001000000000000AAA/p/, you get the Printable View inside a Visualforce Page using a Controller as a proxy to fetch the printable view inline.

printable-sbs

Likely the problem is that the Printable View is served from a different domain (and Setup/native URLs on the salesforce.com domain have different Session ID requirements to Visualforce URLs.)

  • compare the Printable View URL (salesforce.com):

    https://pod.salesforce.com/500000000000000AAA/p
    
  • with a Visualforce Page URL (c.pod.visual.force.com):

    https://c.pod.visual.force.com/CaseDetail?id=500000000000000AAA
    

CaseDetail.cls

public class CaseDetail {
    public String getPrintableView() {
        Id id = ApexPages.currentPage().getParameters().get('id');
        String html = new PageReference('/' + id + '/p').getContent().toString();
        return html.substringAfter('</head>').substringBefore('</html>');
    }
}

CaseDetail.page

<apex:page controller="AccountPrintableView" showHeader="false" applyHtmlTag="false" applyBodyTag="false" renderAs="pdf">
    <html>
        <head>
            <style>
                body {font-family:sans-serif;font-size:12px;}
                table {width:100%;border-collapse:collapse;}
                table tr td {border:1px solid #999;padding:4px;}
                div.printHeader, a.navSkipLink {display: none;}
            </style>
        </head>
        <apex:outputText value="{!PrintableView}" escape="false" />
    </html>
</apex:page>
  • 2
    @bigassforce - Holy batman! Great work! – Eric Mar 4 '15 at 0:48
11

Here's an idea for an overall simpler solution. First, you need a way to expose your information to a 3rd Party (your "Users" without access). This is exactly what Sites are for. Now, creating the pages & code to properly verify users and log them in as well as creating methods to guarantee that you don't expose data you don't want to (sessionCheck methods) are not simple tasks, but they're not too complex either. And isn't too difficult to generalize the concept so it can be reused for ANY Site you need User-Authentication for.

Anyways, here's the main course: To view ANY record with it's standard layout, try the apex:detail tag. And if you need the page rendered as a PDF, use the renderAs="pdf" attribute in the apex:page tag.

So, the case VF page would be as simple as:

<apex:page standardController="Case" extensions="sessionCheckController" renderAs="pdf" action="{!sessionCheck}">
    <apex:detail subject="{!Contact.id}"/>
</apex:page>

With controllerExtension:

public class sessionCheckController{

    private final id recordId;        

    public sessionCheckController(ApexPages.StandardController con){
        this.recordId=con.getId();
        // ...
    }//END init(ApexPages.StandardController con)

    public pageReference sessionCheck(){
        if(!MySite.isSessionValid()) // call the class that manages User-Authentication
            return MySite.getLoginPage();
        return null;  // stay on the page if isSessionValid = true          
    }//END sessionCheck()

}//END sessionCheckController

So, the moral of the story is that once you build the User-Authentication Framework (Apex Class(es) + VF Pages [Login, HomePage, Logout, Register?, Forgot Password?, Error Pages, etc.]), it's EASY to expose any Salesforce data. And this is especially EASY if you just want to show a record's Standard Layout via apex:detail.


Update

Check out this post

How to roll your own authentication for an external Salesforce App?

where I describe how to build the Authentication Interface I mentioned in my above solution. Also, notice there's a link in that post to a second post where I go into even more detail on how the whole structure:

  • a Custom Object to hold User/Session/Site information,
  • an Apex Class called Portal that houses all of the static methods like: login(), logout(), isValidSession(), register(), etc. Also, have static methods to document any action we take -- such as creating a "Session" record for a User documenting whatever we like.
  • have Portal as an Instance act like an instance of ApexPages.StandardController() where the Portal Instance has methods that grab the User + Session info from the SessionCookie, check to see if the session is valid, if so, populate the instance so the instance methods like getUserId() return actual data (because the session is verified. Note we really are mimicking what Salesforce does with ApexPages.StandardControllers. It's great for anybody trying to implement MVC.)

So, again, the idea is we have an sObject called "Portal" that tracks (at least) three types of things:

  1. Site settings - records of this type have a RecordType.Name = 'Site'. Store parameters here that relate to the Site. Example: an integer called Session_Length__c to be used in the static method Portal.isSessionValid() where a Session is "Valid" if CalculatedSessionTime < Session_Length__c.
  2. User Details - records of this type have RecordType.Name = 'User'. Typically, you'll have another object where each record of that object will need a "User" for the Portal. Example: for an 'Account' portal, Portal__c User records for the Account-portal. For the Employee-Portal, we'll have User records lookup to the Employee__c object.
  3. Session info - records of this type have RecordType.Name = 'Session'. This will hold the SessionID and anything else relavent to one particular session. It will have a lookup to a Portal_User record (Portal__c record with RecordType.Name='User'), and this lookup should act like a Master-Detail relationship.

That's the basic setup for the Object, and there is a lot of room for enhancement depending on your need. I prefer having one object with many RecordTypes, but this can eloquently be done with a Custom Object per RecordType.

The next part is the build the Apex Class, Portal, to do all the work. First, have any general action be a static method (like it should be). Examples are generic actions any Authentication Interface will have to do: createUser(), register(), isSessionValid(), login(), changePassword(), logout(), createNewSession(), etc. (As you walk through Use Cases, it'll become evident which methods you need. A Portal-User will get to the Login Screen, then they'll need to login. Then when they get sent to the Home page, we'll need to verify the Session, . . . )

Then, the idea is to have an instance of Portal act like an instance of ApexPages.StandardController. So, the constructor will try to get the Username and SessionId out of the SessionCookie, and do the SOQL queries to get the UserId + whatever else you want to have handy as instance methods.

Another idea I've had for implementation is create an interface for all Portal Controllers.

public interface PortalInterface{
    pageReference sessionCheck();
}

How would I use this? Suppose we have the Visualforce Page Portal_HomePage using Controller Con_Portal_HomePage.

Portal_HomePage:

<apex:page controller="Con_Portal_HomePage" action="{!sessionCheck}">

Con_Portal_HomePage

public class Con_Portal_HomePage implements PortalInterface{
    private Portal portalController;        
    private id userId;

    public Con_Portal_HomePage(){
        this.portalController=Portal.getNewPortalController();
    }//END init()

    private void customInit(){
        // things to initialize if we have a valid session
        this.userId=portalController.getUserId();
    }//END customInit()

    public pageReference sessionCheck(){
        pageReference sessionCheckResult=portalController.getSessionCheck();
        if(sessionCheckResult==null){
            // then the session is valid / stay on the same page
            this.customInit();
        }
        return sessionCheckResult;
    }//END sessionCheck

Now, I could probably jazz this up by creating a Visualforce Template that calls the action method sessionCheck(), but the point is, once logged in, you'll need to verify the session every time a user goes to a different page in your "portal" / Site.

Again, check out the post in the link given above, and make sure to check out the link to the second post too. If you're still reading this, it's probably worth the look. Though this will take some work to initially build, the ideas behind the entire implementation are fairly simple, and the real work lies in mapping out all of the possible Use Cases. And once you have the framework built, it's highly reusable.

Maybe this idea will be packaged up when I get a chance to finely tune it.

  • It is very much against the Master Subscription Agreement to roll your own authentication/licensing work arounds in Public Sites. – Christian Carter Jul 23 '15 at 2:46
  • It's adding an extra layer of security to the data exposed on public Sites. To be used at one's risk. – Scott Pelak Jul 23 '15 at 15:30

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.