/* * * This file is part of Open BlueDragon (OpenBD) CFML Server Engine. * * OpenBD is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * Free Software Foundation,version 3. * * OpenBD is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with OpenBD. If not, see http://www.gnu.org/licenses/ * * Additional permission under GNU GPL version 3 section 7 * * If you modify this Program, or any covered work, by linking or combining * it with any of the JARS listed in the README.txt (or a modified version of * (that library), containing parts covered by the terms of that JAR, the * licensors of this Program grant you additional permission to convey the * resulting work. * README.txt @ http://www.openbluedragon.org/license/README.txt * * http://www.openbluedragon.org/ */ package com.naryx.tagfusion.cfm.document; import java.io.ByteArrayOutputStream; import java.io.File; import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.FilenameFilter; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.io.Serializable; import java.io.StringReader; import java.io.StringWriter; import java.net.URI; import java.net.URISyntaxException; import java.util.HashMap; import java.util.List; import javax.xml.parsers.DocumentBuilder; import javax.xml.parsers.DocumentBuilderFactory; import org.apache.commons.io.IOUtils; import org.apache.http.Header; import org.apache.http.HttpHost; import org.apache.http.HttpResponse; import org.apache.http.auth.AuthScope; import org.apache.http.auth.UsernamePasswordCredentials; import org.apache.http.client.methods.HttpGet; import org.apache.http.conn.params.ConnRoutePNames; import org.apache.http.impl.client.DefaultHttpClient; import org.aw20.io.StreamUtil; import org.w3c.dom.Document; import org.w3c.dom.NamedNodeMap; import org.w3c.dom.Node; import org.w3c.dom.NodeList; import org.w3c.tidy.Tidy; import org.xhtmlrenderer.pdf.DefaultPDFCreationListener; import org.xhtmlrenderer.pdf.ITextFontResolver; import org.xhtmlrenderer.pdf.ITextRenderer; import org.xhtmlrenderer.pdf.PDFEncryption; import org.xml.sax.InputSource; import com.lowagie.text.DocumentException; import com.lowagie.text.FontFactory; import com.lowagie.text.pdf.BaseFont; import com.lowagie.text.pdf.PdfName; import com.lowagie.text.pdf.PdfString; import com.lowagie.text.pdf.PdfWriter; import com.nary.io.FileUtils; import com.nary.io.multiOutputStream; import com.naryx.tagfusion.cfm.engine.cfBinaryData; import com.naryx.tagfusion.cfm.engine.cfData; import com.naryx.tagfusion.cfm.engine.cfEngine; import com.naryx.tagfusion.cfm.engine.cfSession; import com.naryx.tagfusion.cfm.engine.cfmBadFileException; import com.naryx.tagfusion.cfm.engine.cfmRunTimeException; import com.naryx.tagfusion.cfm.engine.dataNotSupportedException; import com.naryx.tagfusion.cfm.tag.cfOptionalBodyTag; import com.naryx.tagfusion.cfm.tag.cfTag; import com.naryx.tagfusion.cfm.tag.cfTagReturnType; import com.naryx.tagfusion.cfm.tag.tagLocator; import com.naryx.tagfusion.cfm.tag.tagReader; import com.naryx.tagfusion.cfm.xml.cfXmlData; import com.naryx.tagfusion.cfm.xml.parse.NoValidationResolver; import com.naryx.tagfusion.xmlConfig.xmlCFML; public class cfDOCUMENT extends cfTag implements cfOptionalBodyTag, Serializable { private static final long serialVersionUID = 1; private static final String TAG_NAME = "CFDOCUMENT"; private String endMarker = null; public static final String CFDOCUMENT_KEY = "CFDOCUMENT"; private static String[] defaultWindowsFontDirs = { "C:\\Windows\\Fonts", "C:\\WINNT\\Fonts" }; private static String[] defaultOtherFontDirs = { "/usr/X/lib/X11/fonts/TrueType", "/usr/openwin/lib/X11/fonts/TrueType", "/usr/share/fonts/default/TrueType", "/usr/X11R6/lib/X11/fonts/ttf", "/usr/X11R6/lib/X11/fonts/truetype", "/usr/X11R6/lib/X11/fonts/TTF" }; private static String [] defaultFontDirs; public static void init( xmlCFML configFile ) { String fontDirs = cfEngine.getConfig().getString( "server.fonts.dirs", "" ); if ( fontDirs.length() == 0 ) { // no fonts configured, set defaults StringBuilder defaultFontDirsList = new StringBuilder(); if ( cfEngine.WINDOWS ) { for ( int i = 0; i < defaultWindowsFontDirs.length; i++ ) { if ( FileUtils.exists( defaultWindowsFontDirs[ i ] ) ) { if ( defaultFontDirsList.length() > 0 ) { // not the first defaultFontDirsList.append( ',' ); } defaultFontDirsList.append( defaultWindowsFontDirs[ i ] ); } } } else { for ( int i = 0; i < defaultOtherFontDirs.length; i++ ) { if ( FileUtils.exists( defaultOtherFontDirs[ i ] ) ) { if ( defaultFontDirsList.length() > 0 ) { // not the first defaultFontDirsList.append( ',' ); } defaultFontDirsList.append( defaultOtherFontDirs[ i ] ); } } } if ( defaultFontDirsList.length() > 0 ) { cfEngine.getConfig().setData( "server.fonts.dirs", defaultFontDirsList.toString() ); } fontDirs = defaultFontDirsList.toString(); } defaultFontDirs = fontDirs.split(","); for ( int i = 0; i < defaultFontDirs.length; i++ ){ FontFactory.registerDirectory( defaultFontDirs[i].toString() ); } } public boolean doesTagHaveEmbeddedPoundSigns(){ return false; } public String getEndMarker(){ return endMarker; } public void setEndTag() { endMarker = null; } public void lookAheadForEndTag(tagReader inFile) { endMarker = (new tagLocator(TAG_NAME, inFile)).findEndMarker(); } /* TODO: these attributes are still to be supported fontEmbed = "yes|no|selective" // use ITextResolver bookmark = "yes|no" scale = "percentage less than 100" localUrl = "yes|no" */ protected void defaultParameters( String _tag ) throws cfmBadFileException { defaultAttribute( "ENCRYPTION", "none" ); defaultAttribute( "FORMAT", "PDF" ); defaultAttribute( "FONTEMBED", "true" ); defaultAttribute( "MIMETYPE", "text/html" ); defaultAttribute( "ORIENTATION", "PORTRAIT" ); defaultAttribute( "OVERWRITE", "false" ); defaultAttribute( "PAGETYPE", "letter" ); defaultAttribute( "UNIT", "IN" ); defaultAttribute( "USERAGENT", "OpenBD" ); defaultAttribute( "BACKGROUNDVISIBLE", "true" ); parseTagHeader( _tag ); } public cfTagReturnType render( cfSession _Session ) throws cfmRunTimeException { if ( !getDynamic( _Session, "FORMAT" ).getString().equalsIgnoreCase( "PDF" ) ){ throw newRunTimeException( "Invalid FORMAT value. Only \"PDF\" is supported." ); } if ( containsAttribute( "SRC" ) && containsAttribute( "SRCFILE" ) ){ throw newRunTimeException( "Invalid attribute combination. Either the SRC or SRCFILE attribute must be specified but not both" ); } ITextRenderer renderer = new ITextRenderer(); CreationListener listener = new CreationListener(getDynamic(_Session, "AUTHOR"),getDynamic(_Session, "TITLE"),getDynamic(_Session, "SUBJECT"),getDynamic(_Session, "KEYWORDS")); renderer.setListener(listener); resolveFonts( _Session, renderer ); if ( _Session.getDataBin( CFDOCUMENT_KEY ) != null ){ throw newRunTimeException( "CFDOCUMENT cannot be embedded within another CFDOCUMENT tag" ); } _Session.setDataBin( CFDOCUMENT_KEY, new DocumentContainer() ); String renderedBody = renderToString( _Session ).getOutput(); try{ DocumentContainer container = (DocumentContainer) _Session.getDataBin( CFDOCUMENT_KEY ); List<DocumentSection> sections = container.getSections(); if ( sections.size() == 0 ){ // if no sections are specified then construct one from this tag DocumentSection section = new DocumentSection(); section.setHeader( container.getMainHeader(), container.getMainHeaderAlign() ); section.setFooter( container.getMainFooter(), container.getMainFooterAlign() ); if ( renderedBody.length() == 0 && !( containsAttribute( "SRC" ) || containsAttribute( "SRCFILE" ) ) ){ throw newRunTimeException( "Cannot create a PDF from an empty document!" ); } String src = containsAttribute( "SRC" ) ? getDynamic( _Session, "SRC" ).getString() : null; String srcFile = containsAttribute( "SRCFILE" ) ? getDynamic( _Session, "SRCFILE" ).getString() : null; section.setSources( src, srcFile, renderedBody ); appendSectionAttributes( _Session, section ); sections.add( section ); } DocumentSettings settings = getDocumentSettings( _Session, container ); // If there is more than 1 section and page counters are used that need special // processing then we need to do an initial conversion of the HTML to PDF to // determine how many pages are created per section and how many pages are created total. if ((sections.size() > 1) && (container.usesTotalPageCounters())) preparePageCounters( _Session, renderer, sections, settings ); preparePDF( _Session, renderer, sections, settings ); return cfTagReturnType.NORMAL; }finally{ _Session.deleteDataBin( CFDOCUMENT_KEY ); } } private void resolveFonts( cfSession _Session, ITextRenderer _renderer ) throws dataNotSupportedException, cfmRunTimeException{ ITextFontResolver resolver = _renderer.getFontResolver(); boolean embed = getDynamic( _Session, "FONTEMBED" ).getBoolean(); for ( int i = 0; i < defaultFontDirs.length; i++ ){ File nextFontDir = new File( defaultFontDirs[i] ); File[] fontFiles = nextFontDir.listFiles( new FilenameFilter() { public boolean accept( File _dir, String _name ){ String name = _name.toLowerCase(); return name.endsWith( ".otf" ) || name.endsWith( ".ttf" ); } }); if ( fontFiles != null ){ for ( int f = 0; f < fontFiles.length; f++ ){ try{ resolver.addFont( fontFiles[f].getAbsolutePath(), BaseFont.IDENTITY_H, embed ); }catch( Exception ignored ){} // ignore fonts that can't be added } } } } /* * preparePageCounters * * This method is expensive because it requires us to convert the HTML to PDF to * determine the total page counters. This method should only be called if there is more * than one section and one of the following page counter variables is being used: * * 1. TotalPageCount * 2. TotalSectionPageCount */ private void preparePageCounters( cfSession _Session, ITextRenderer _renderer, List<DocumentSection> _sections, DocumentSettings _settings ) throws cfmRunTimeException{ OutputStream pdfOut = null; try{ pdfOut = new NullOutputStream(); DocumentSection nextSection = _sections.get( 0 ); if (nextSection.pageCounterConflict()) throw newRunTimeException("OpenBD doesn't support currentpagenumber and currentsectionpagenumber in same section."); String renderedBody = getRenderedBody( _Session, nextSection, _settings, _sections.size() ); _renderer.setDocument( getDocument( renderedBody ), nextSection.getBaseUrl( _Session ) ); _renderer.layout(); _renderer.createPDF( pdfOut, false ); int currentPageNumber = _renderer.getWriter().getCurrentPageNumber(); nextSection.setTotalSectionPageCount(currentPageNumber); int totalPageCount = currentPageNumber; for ( int i = 1; i < _sections.size(); i++ ){ nextSection = _sections.get( i ); if (nextSection.pageCounterConflict()) throw newRunTimeException("OpenBD doesn't support currentpagenumber and currentsectionpagenumber in same section."); renderedBody = getRenderedBody( _Session, nextSection, _settings, _sections.size() ); _renderer.setDocument( getDocument( renderedBody ), nextSection.getBaseUrl( _Session ) ); _renderer.layout(); _renderer.writeNextDocument( _renderer.getWriter().getCurrentPageNumber()+1 ); currentPageNumber = _renderer.getWriter().getCurrentPageNumber(); nextSection.setTotalSectionPageCount(currentPageNumber-totalPageCount); totalPageCount = currentPageNumber; } for ( int i = 0; i < _sections.size(); i++ ){ nextSection = _sections.get( i ); nextSection.setTotalPageCount(totalPageCount); } } catch (DocumentException e) { throw newRunTimeException( "Failed to create PDF due to DocumentException: " + e.getMessage() ); }finally{ if ( pdfOut != null )try{ pdfOut.close(); }catch( IOException ignored ){} } } private void preparePDF( cfSession _Session, ITextRenderer _renderer, List<DocumentSection> _sections, DocumentSettings _settings ) throws cfmRunTimeException{ OutputStream pdfOut = null; try{ ByteArrayOutputStream bos = null; if ( containsAttribute( "FILENAME" ) ){ File pdfFile = new File( getDynamic( _Session, "FILENAME" ).getString() ); if ( pdfFile.exists() && !getDynamic( _Session, "OVERWRITE" ).getBoolean() ){ throw newRunTimeException( "PDF file already exists and overwrite is disabled." ); } pdfOut = cfEngine.thisPlatform.getFileIO().getFileOutputStream(pdfFile); if ( containsAttribute( "NAME" ) ){ bos = new ByteArrayOutputStream(); pdfOut = new multiOutputStream( pdfOut, bos ); } }else if ( containsAttribute( "NAME" ) ){ pdfOut = new ByteArrayOutputStream(); }else{ _Session.resetBuffer(); // The SAVEASNAME attribute as been tested with the following: // // IE8 - Page/Save As : FAILS // IE8 - PDF plugin save image : FAILS // Firefox 3.5.8 - File/Save Page As : WORKS // Firefox 3.5.8 - PDF plugin save image : FAILS // String saveAsName; if ( containsAttribute( "SAVEASNAME" ) ){ saveAsName = getDynamic( _Session, "SAVEASNAME" ).toString(); } else { // Extract the filename from the path and use it as the save as name saveAsName = _Session.REQ.getServletPath(); int slash = saveAsName.lastIndexOf('/'); if ( slash != -1 ) saveAsName = saveAsName.substring(slash+1); int dot = saveAsName.lastIndexOf('.'); if ( dot != -1 ) saveAsName = saveAsName.substring(0,dot) + ".pdf"; } pdfOut = new SessionOutputStream( _Session, saveAsName ); } // handle encryption/password if attributes are set if ( containsAttribute("OWNERPASSWORD") || containsAttribute("USERPASSWORD") || containsAttribute("PERMISSIONS") || !getDynamic( _Session, "ENCRYPTION" ).getString().equalsIgnoreCase("none")){ PDFEncryption mEnc = new PDFEncryption(); setPermissions( _Session, mEnc ); _renderer.setPDFEncryption( mEnc ); } DocumentSection nextSection = _sections.get( 0 ); String renderedBody = getRenderedBody( _Session, nextSection, _settings, _sections.size() ); _renderer.setDocument( getDocument( renderedBody ), nextSection.getBaseUrl( _Session ) ); _renderer.layout(); _renderer.createPDF( pdfOut, false ); for ( int i = 1; i < _sections.size(); i++ ){ nextSection = _sections.get( i ); renderedBody = getRenderedBody( _Session, nextSection, _settings, _sections.size() ); _renderer.setDocument( getDocument( renderedBody ), nextSection.getBaseUrl( _Session ) ); _renderer.layout(); if ( nextSection.usesCurrentPageNumber() ) { // uses currentpagenumber so start page numbering with current page number + 1 _renderer.writeNextDocument( _renderer.getWriter().getCurrentPageNumber() + 1 ); } else { // uses currentsectionpagenumber so start page numbering with 1 _renderer.writeNextDocument( 1 ); } } _renderer.finishPDF(); if ( pdfOut instanceof SessionOutputStream ){ if( ( (SessionOutputStream) pdfOut).getException() != null ){ throw ( (SessionOutputStream) pdfOut).getException(); } _Session.pageFlush(); _Session.abortPageProcessing(); } // Add the data to our session if ( containsAttribute( "NAME" ) ) { if ( bos != null ) _Session.setData(getDynamic(_Session, "NAME").toString(), new cfBinaryData(bos.toByteArray())); else _Session.setData(getDynamic(_Session, "NAME").toString(), new cfBinaryData(((ByteArrayOutputStream)pdfOut).toByteArray())); } } catch (DocumentException e) { throw newRunTimeException( "Failed to create PDF due to DocumentException: " + e.getMessage() ); } catch ( IOException e ) { throw newRunTimeException( "Error writing PDF to file. Check the file exists and can be written to." ); }finally{ if ( pdfOut != null )try{ pdfOut.close(); }catch( IOException ignored ){} } } private String getRenderedBody( cfSession _Session, DocumentSection _section, DocumentSettings _settings, int _numSections ) throws cfmRunTimeException{ String renderedBody = _section.getBody(); if ( renderedBody.length() != 0 ){ // If the section had a mimetype specified and it's text/plain or if it didn't have a mimetype // specified and the document mimetype was set to text/plain then treat the body as plain text. if ((( _section.getMimeType() != null ) && (_section.getMimeType().equalsIgnoreCase("text/plain"))) || (( _section.getMimeType() == null ) && (getDynamic(_Session,"MIMETYPE").getString().equalsIgnoreCase("text/plain")))) renderedBody = getXHTML( "<pre>" + escapeHtmlChars(renderedBody) + "</pre>" ); else renderedBody = getXHTML( renderedBody ); }else{ renderedBody = getXHTML( retrieveDocument( _Session, _section, _settings ) ); } renderedBody = insertStyles( _Session, renderedBody, _section, _settings, getDynamic(_Session, "BACKGROUNDVISIBLE").getBoolean(), _numSections ); return renderedBody; } public Document getDocument( String _renderedBody ) throws cfmRunTimeException{ try{ DocumentBuilder builder; InputSource is = new InputSource( new StringReader( _renderedBody ) ); Document doc; DocumentBuilderFactory builderFactory = DocumentBuilderFactory.newInstance(); builderFactory.setValidating( false ); builder = builderFactory.newDocumentBuilder(); builder.setEntityResolver( new NoValidationResolver() ); doc = builder.parse( is ); return doc; } catch (Exception e) { throw newRunTimeException( "Failed to create valid xhtml document due to " + e.getClass().getName() + ": " + e.getMessage() ); } } private String getXHTML( String _html ){ Tidy tidy = new Tidy(); tidy.setQuiet( true ); tidy.setNumEntities( true ); tidy.setShowWarnings( false ); StringWriter result = new StringWriter(); tidy.setMakeClean( true ); tidy.setXHTML( true ); tidy.parse( new StringReader( _html ), result ); return result.toString(); } private String retrieveDocument( cfSession _Session, DocumentSection _section, DocumentSettings _settings ) throws dataNotSupportedException, cfmRunTimeException{ String src = _section.getSrc(); if ( src != null ){ // if this is a file:// url then just handle it as a SRCFILE rather // than pass it through HttpClient if ( src.startsWith( "file://" ) ){ return retrieveLocalFile( _Session, src.substring( 8 ) ); }else if ( src.startsWith( "http://") || src.startsWith( "https://") ){ return retrieveHttp( _Session, src, _section, _settings ); }else{ return retrieveHttp( _Session, makeAbsoluteUrl(src, _Session), _section, _settings ); } }else{ return retrieveLocalFile( _Session, _section.getSrcFile() ); } } private static String makeAbsoluteUrl(String relativeUrl, cfSession _Session) { boolean serverRelative = false; if (relativeUrl.startsWith("/")) serverRelative = true; StringBuffer base = new StringBuffer(_Session.REQ.getScheme()); base.append("://"); base.append(_Session.REQ.getServerName()); if (_Session.REQ.getServerPort() != 80) base.append(":" + _Session.REQ.getServerPort()); if (serverRelative) { base.append(relativeUrl); } else { if (_Session.REQ.getContextPath().equals("")) base.append("/"); base.append(_Session.REQ.getContextPath()); base.append(_Session.REQ.getServletPath().substring( 0, _Session.REQ.getServletPath().lastIndexOf("/"))); base.append("/" + relativeUrl); } return base.toString(); } private String retrieveLocalFile( cfSession _Session, String _filepath ) throws cfmRunTimeException{ File file = new File( _filepath ); if (!file.exists()) file = new File(_Session.getPresentDirectory(), _filepath); FileInputStream fin = null; try{ fin = new FileInputStream( file ); return handleDocument( _Session, fin, null ); } catch (FileNotFoundException e) { throw newRunTimeException( "Invalid file specified. " + _filepath + " could not be found" ); } catch (IOException e) { throw newRunTimeException( "Failed to read specified file " + _filepath + ". Check the sufficient permissions have been set to permit reading of this file." ); }finally{ if ( fin != null )try{ fin.close(); }catch( Exception ignored ){} } } private String retrieveHttp( cfSession _Session, String _src, DocumentSection _section, DocumentSettings _defaultSettings ) throws dataNotSupportedException, cfmRunTimeException{ DefaultHttpClient httpClient = new DefaultHttpClient(); HttpGet method = new HttpGet(); try { method.setURI( new URI( _src ) ); if ( _section.getUserAgent() != null ){ method.setHeader( "User-Agent", _section.getUserAgent() ); }else{ method.setHeader( "User-Agent", _defaultSettings.getUserAgent() ); } // HTTP basic authentication if ( _section.getAuthPassword() != null ){ httpClient.getCredentialsProvider().setCredentials( AuthScope.ANY, new UsernamePasswordCredentials( _section.getAuthUser(), _section.getAuthPassword() ) ); } // proxy support if ( _defaultSettings.getProxyHost() != null ){ HttpHost proxy = new HttpHost( _defaultSettings.getProxyHost() , _defaultSettings.getProxyPort() ); httpClient.getParams().setParameter( ConnRoutePNames.DEFAULT_PROXY, proxy ); if ( _defaultSettings.getProxyUser() != null ){ httpClient.getCredentialsProvider().setCredentials( new AuthScope( _defaultSettings.getProxyHost() , _defaultSettings.getProxyPort() ), new UsernamePasswordCredentials( _defaultSettings.getProxyUser(), _defaultSettings.getProxyPassword() ) ); } } HttpResponse response; response = httpClient.execute( method ); if ( response.getStatusLine().getStatusCode() == 200 ){ String charset = null; Header contentType = response.getFirstHeader( "Content-type" ); if ( contentType != null ){ String value = contentType.getValue(); int indx = value.indexOf( "charset=" ); if ( indx > 0 ){ charset = value.substring( indx+8 ).trim(); } } return handleDocument( _Session, response.getEntity().getContent(), charset ); }else{ throw newRunTimeException( "Failed to retrieve document from source. HTTP status code " + response.getStatusLine().getStatusCode() + " was returned" ); } // throw newRunTimeException( "Failed to retrieve document from " + _src + " due to HttpException: " + e.getMessage() ); } catch (URISyntaxException e) { throw newRunTimeException( "Error retrieving document via http: " + e.getMessage() ); } catch (IOException e) { throw newRunTimeException( "Error retrieving document via http: " + e.getMessage() ); } } private DocumentSettings getDocumentSettings( cfSession _Session, DocumentContainer _container ) throws dataNotSupportedException, cfmRunTimeException{ DocumentSettings settings = new DocumentSettings(); // get UNIT value String unit = getDynamic( _Session, "UNIT" ).getString().toLowerCase(); if ( !unit.equals( "in" ) && !unit.equals( "cm" ) ){ throw newRunTimeException( "Invalid UNIT value. Valid values include \"IN\" and \"CM\"." ); } settings.setUnit( unit ); // set margins if ( containsAttribute( "MARGINTOP" ) ){ settings.setMarginTop( getDynamic( _Session, "MARGINTOP" ).getString() ); } if ( containsAttribute( "MARGINLEFT" ) ){ settings.setMarginLeft( getDynamic( _Session, "MARGINLEFT" ).getString() ); } if ( containsAttribute( "MARGINRIGHT" ) ){ settings.setMarginRight( getDynamic( _Session, "MARGINRIGHT" ).getString() ); } if ( containsAttribute( "MARGINBOTTOM" ) ){ settings.setMarginBottom( getDynamic( _Session, "MARGINBOTTOM" ).getString() ); } // page type and size String pageType = getDynamic( _Session, "PAGETYPE" ).getString().toLowerCase(); String pageSize = "a4"; boolean landscape = false; // default to portrait if ( containsAttribute( "ORIENTATION" ) ){ String orientStr = getDynamic( _Session, "ORIENTATION" ).getString(); if ( orientStr.equalsIgnoreCase( "LANDSCAPE" ) ){ landscape = true; }else if ( !orientStr.equalsIgnoreCase( "PORTRAIT" ) ){ throw newRunTimeException( "Invalid ORIENTATION value. Valid values include \"PORTRAIT\" and \"LANDSCAPE\"." ); } } if ( pageType.equals( "a4" ) || pageType.equals( "a5" ) || pageType.equals( "b4" ) || pageType.equals( "legal" ) || pageType.endsWith( "letter" )){ pageSize = pageType; if ( landscape ){ pageSize += " landscape"; } }else{ String width = null; String height = null; if ( pageType.equals( "b5" ) ){ width = "7in"; height = "9.88in"; }else if ( pageType.equals( "b5-jis" ) ){ width = "7.19in"; height = "10.13in"; }else if ( pageType.equals( "b4-jis" ) ){ width = "10.13in"; height = "14.31in"; }else if ( pageType.equals( "custom" ) ){ if ( !containsAttribute( "PAGEHEIGHT" ) || !containsAttribute( "PAGEWIDTH" ) ){ throw newRunTimeException( "Missing PAGEHEIGHT/PAGEWIDTH attribute(s). Both must be specified when specifying a CUSTOM page size" ); } width = getDynamic( _Session, "PAGEWIDTH" ).getString() + unit; height = getDynamic( _Session, "PAGEHEIGHT" ).getString() + unit; }else{ throw newRunTimeException( "Invalid PAGETYPE value." ); } if ( landscape ){ String tmp = height; height = width; width = tmp; } pageSize = width + " " + height; } settings.setPageSize( pageSize ); // proxy details if ( containsAttribute( "PROXYHOST" ) ){ String proxyHost = getDynamic( _Session, "PROXYHOST" ).getString(); int proxyPort = 80; if ( containsAttribute( "PROXYPORT" ) ){ proxyPort = getDynamic( _Session, "PROXYPORT" ).getInt(); } String proxyUser = null; String proxyPassword = null; if ( containsAttribute( "PROXYUSER" ) ){ proxyUser = getDynamic( _Session, "PROXYUSER" ).getString(); proxyPassword = getDynamic( _Session, "PROXYPASSWORD" ).getString(); } settings.setProxyDetails( proxyHost, proxyPort, proxyUser, proxyPassword ); } return settings; } @SuppressWarnings("deprecation") private String handleDocument( cfSession _Session, InputStream _in, String _charset ) throws IOException, dataNotSupportedException, cfmRunTimeException{ String mimeType = getDynamic( _Session, "MIMETYPE" ).getString().toLowerCase(); String charset = _charset; if ( charset == null ){ charset = "ISO-8859-1"; } if ( mimeType.equals( "text/html" ) ){ return IOUtils.toString( _in, charset ); }else if ( mimeType.equals( "text/plain" ) ){ String plainTxt = IOUtils.toString( _in, charset ); return "<pre>" + escapeHtmlChars(plainTxt) + "</pre>"; }else if ( mimeType.startsWith( "image/" ) ){ File tmpFile = File.createTempFile( "cfdoc", '.' + mimeType.substring( mimeType.indexOf( '/' )+1 ) ); OutputStream fout = cfEngine.thisPlatform.getFileIO().getFileOutputStream( tmpFile ); StreamUtil.copyTo( _in, fout ); return "<img src=\"" + tmpFile.toURL() + "\"/>"; }else{ throw newRunTimeException( "Invalid MIMETYPE value. Supported values include text/html, text/plain, image/jpg, image/gif, image/png and image/bmp" ); } } /* * escapeHTMLChars * * This is used to escape the HTML chars in a plain text file so * that JTidy and flying saucer ignore them. */ private static String escapeHtmlChars(String content) { char[] old = new char[] { '&', '<', '>', '\"' }; String[] replacements = new String[] { "&", "<", ">", """ }; int strLen = content.length(); int charsLen = old.length; StringBuilder buffer = new StringBuilder(content); StringBuilder writer = new StringBuilder(strLen); char nextChar; boolean foundCh; for (int i = 0; i < strLen; i++) { nextChar = buffer.charAt(i); foundCh = false; for (int j = 0; j < charsLen; j++) { if (nextChar == old[j]) { writer.append(replacements[j]); foundCh = true; } } if (!foundCh) { writer.append(nextChar); } } return writer.toString();//.Replace("\n", "<br>"); } private void setPermissions( cfSession _Session, PDFEncryption _pdfEnc ) throws cfmRunTimeException{ // apply encryption String encryption = getDynamic( _Session, "ENCRYPTION" ).getString().toLowerCase(); if ( encryption.equals( "40" ) || encryption.equals( "40-bit" ) ){ _pdfEnc.setEncryptionType( PdfWriter.STANDARD_ENCRYPTION_40); }else if ( encryption.equals( "128" ) || encryption.equals( "128-bit" ) ){ _pdfEnc.setEncryptionType( PdfWriter.STANDARD_ENCRYPTION_128); }else if ( encryption.equals( "aes" ) ){ _pdfEnc.setEncryptionType( PdfWriter.ENCRYPTION_AES_128); }else if ( !encryption.equals( "none" ) ){ throw newRunTimeException( "Invalid ENCRYPTION value. Supported values include \"40-bit\", \"128-bit\", \"AES\" and \"none\"" ); } // Default to no permissions int permissionsMask = 0; if ( containsAttribute( "PERMISSIONS" ) ){ String [] permissions = getDynamic( _Session, "PERMISSIONS" ).getString().toLowerCase().split( "," ); if ( permissions.length > 0 ){ for ( int i = 0; i < permissions.length; i++ ){ String nextPermission = permissions[i]; if ( nextPermission.equals( "allowprinting" ) ){ permissionsMask |= PdfWriter.ALLOW_PRINTING; }else if ( nextPermission.equals( "allowmodifycontents" ) ){ permissionsMask |= PdfWriter.ALLOW_MODIFY_CONTENTS; }else if ( nextPermission.equals( "allowcopy" ) ){ permissionsMask |= PdfWriter.ALLOW_COPY; }else if ( nextPermission.equals( "allowmodifyannotations" ) ){ permissionsMask |= PdfWriter.ALLOW_MODIFY_ANNOTATIONS; }else if ( nextPermission.equals( "allowscreenreaders" ) ){ if (_pdfEnc.getEncryptionType() == PdfWriter.STANDARD_ENCRYPTION_40) throw newRunTimeException("AllowScreenReaders is not valid with 40-bit encryption"); permissionsMask |= PdfWriter.ALLOW_SCREENREADERS; }else if ( nextPermission.equals( "allowassembly" ) ){ if (_pdfEnc.getEncryptionType() == PdfWriter.STANDARD_ENCRYPTION_40) throw newRunTimeException("AllowAssembly is not valid with 40-bit encryption"); permissionsMask |= PdfWriter.ALLOW_ASSEMBLY; }else if ( nextPermission.equals( "allowdegradedprinting" ) ){ if (_pdfEnc.getEncryptionType() == PdfWriter.STANDARD_ENCRYPTION_40) throw newRunTimeException("AllowDegradedPrinting is not valid with 40-bit encryption"); permissionsMask |= PdfWriter.ALLOW_DEGRADED_PRINTING; }else if ( nextPermission.equals( "allowfillin" ) ){ if (_pdfEnc.getEncryptionType() == PdfWriter.STANDARD_ENCRYPTION_40) throw newRunTimeException("AllowFillIn is not valid with 40-bit encryption"); permissionsMask |= PdfWriter.ALLOW_FILL_IN; }else{ throw newRunTimeException( "Invalid permissions value: " + nextPermission ); } } } } // Set the allowed permissions _pdfEnc.setAllowedPrivileges(permissionsMask); if ( containsAttribute("OWNERPASSWORD") ) _pdfEnc.setOwnerPassword( getDynamic( _Session, "OWNERPASSWORD" ).getString().getBytes() ); if ( containsAttribute("USERPASSWORD") ) _pdfEnc.setUserPassword( getDynamic( _Session, "USERPASSWORD" ).getString().getBytes() ); } /* * removeBackground * * Removes all of the background items like the 'bgcolor' attribute from * the XHTML body. */ private String removeBackground(String _body) { try { // Parse the XHTML body into an XML object cfXmlData content = cfXmlData.parseXml(_body, true, null); // Remove the background items from the top node and all of its children Node node = content.getXMLNode(); removeBackground(node); // Convert the XML object back into a string _body = content.toString(); } catch (Exception e) { } return _body; } /* * removeBackground * * Removes all of the background items like the 'bgcolor' attribute from * the XML node and recursively calls itself to remove them from all child * nodes too. */ private void removeBackground(Node _node) { // Remove any background items from the node. // For now we only remove the 'bgcolor' attribute. NamedNodeMap attributes = _node.getAttributes(); if ( (attributes != null) && (attributes.getNamedItem("bgcolor") != null) ) attributes.removeNamedItem("bgcolor"); // If the node has children then make recursive calls to remove the // background items from the children too. if (_node.hasChildNodes()) { NodeList children =_node.getChildNodes(); for (int i = 0; i < children.getLength(); i++) removeBackground(children.item(i)); } } /* * extractTagAttributes * * Extracts the attributes for the tag with the specified name. */ private HashMap<String, String> extractTagAttributes( String _tagName, String _xhtml ) { HashMap<String, String> attributes = new HashMap<String, String>(); try { // Parse the XHTML body into an XML object cfXmlData content = cfXmlData.parseXml(_xhtml, true, null); // Find the node for the specified tag Node node = findNode(content.getXMLNode(), _tagName); if ( node != null) { // Copy the node's attributes (if any) into the HashMap NamedNodeMap nodeAttributes = node.getAttributes(); if (nodeAttributes != null) { for (int i=0; i < nodeAttributes.getLength(); i++) attributes.put(nodeAttributes.item(i).getNodeName(), nodeAttributes.item(i).getNodeValue()); } } } catch (Exception e) { } return attributes; } /* * findNode * * Search the node and all child nodes for the node with the specified name. */ private Node findNode(Node _node, String _tagName) { String name = _node.getNodeName(); if ((name != null) && name.equals(_tagName)) return _node; if (_node.hasChildNodes()) { NodeList children =_node.getChildNodes(); for (int i = 0; i < children.getLength(); i++) { Node n = findNode(children.item(i), _tagName); if ( n != null) return n; } } return null; } private String insertStyles( cfSession _Session, String _body, DocumentSection _section, DocumentSettings _settings, boolean _backgroundVisible, int _numSections ) throws cfmRunTimeException{ int headStart = _body.indexOf( "<head>" ); if ( headStart < 0 ){ return _body; } HashMap<String, String> bodyTagAttributes = null; if ( !_backgroundVisible ) { // The background is set to not visible so remove all // the background items like the 'bgcolor' attribute. _body = removeBackground(_body); headStart = _body.indexOf( "<head>" ); } else { // For some reason the BODY bgcolor attribute is being ignored so let's extract // it and set the background color using CSS. We'll extract all the body tag's // attributes since we might need them in the future. bodyTagAttributes = extractTagAttributes("body", _body); } StringBuilder styleBlock = new StringBuilder(); styleBlock.append( "<style>\n" ); styleBlock.append( "@page{\n" ); styleBlock.append( getMargins( _Session, _section, _settings ) ); String header = replaceHeaderFooterVariables(_section, _section.getHeader(), _numSections); String footer = replaceHeaderFooterVariables(_section, _section.getFooter(), _numSections); insertHeaderFooter( _Session, styleBlock, "top", _section.getHeaderAlign(), header, _backgroundVisible, _section.getMimeType() ); insertHeaderFooter( _Session, styleBlock, "bottom", _section.getFooterAlign(), footer, _backgroundVisible, _section.getMimeType() ); styleBlock.append( "size: " ); styleBlock.append( _settings.getPageSize() ); styleBlock.append( ";\n" ); styleBlock.append( "}\n" ); if ((bodyTagAttributes != null) && ( bodyTagAttributes.containsKey("bgcolor"))) { styleBlock.append( "body { background-color: " ); styleBlock.append( bodyTagAttributes.get("bgcolor") ); styleBlock.append( "; }\n"); } styleBlock.append( "</style>\n" ); return _body.substring( 0, headStart+6 ) + styleBlock + _body.substring( headStart+6 ); } /* * replaceHeaderFooterVariables * * Replace the header/footer page count variables with the appropriate values. */ private String replaceHeaderFooterVariables(DocumentSection _section, String _content, int _numSections) { if (_content == null) return null; _content = _content.replaceAll("BD:CURRENTPAGENUMBER", "\" counter(page) \""); _content = _content.replaceAll("BD:CURRENTSECTIONPAGENUMBER", "\" counter(page) \""); if ( _numSections == 1 ){ // There's only 1 section so we can use the page and pages counters. _content = _content.replaceAll("BD:TOTALPAGECOUNT", "\" counter(pages) \""); _content = _content.replaceAll("BD:TOTALSECTIONPAGECOUNT", "\" counter(pages) \""); } else { // There's more than 1 section so we need to use the values calculated by preparePageCounters(). _content = _content.replaceAll("BD:TOTALPAGECOUNT", Integer.toString(_section.getTotalPageCount())); _content = _content.replaceAll("BD:TOTALSECTIONPAGECOUNT", Integer.toString(_section.getTotalSectionPageCount())); } return _content; } private void insertHeaderFooter( cfSession _Session, StringBuilder _sb, String _position, String _align, String _content, boolean _backgroundVisible, String _sectionMimeType ) throws cfmRunTimeException{ if ( _content != null ){ _sb.append( "@" ); _sb.append( _position ); _sb.append( "-" ); _sb.append( _align ); _sb.append( "{\nwhite-space: pre;\n" ); // this is necessary so escaped newlines in the content are displayed properly // If the section had a mimetype specified and it's text/plain or if it didn't have a mimetype // specified and the document mimetype was set to text/plain then treat the body as plain text. if ((( _sectionMimeType != null ) && (_sectionMimeType.equalsIgnoreCase("text/plain"))) || (( _sectionMimeType == null ) && (getDynamic(_Session,"MIMETYPE").getString().equalsIgnoreCase("text/plain")))) { _sb.append( "content: " ); _sb.append( convertPlainTextToCSSContent(_content) ); } else { HashMap<String, String> properties = new HashMap<String,String>(); String xhtml = getHeaderFooterXHTML(_content, properties); if ( _backgroundVisible && properties.containsKey("background-color")) { _sb.append( "background-color: " ); _sb.append( properties.get("background-color") ); _sb.append( ";\n" ); } _sb.append( "content: " ); _sb.append( xhtml ); } _sb.append( ";\n}\n" ); } } private String getHeaderFooterXHTML(String _content, HashMap<String,String> _properties) { // Convert to XHTML String xhtml = getXHTML(_content); // Find the beginning of the body tag and body text (starts with double quotes) int beginBody = xhtml.indexOf("<body"); if ( beginBody >= 0 ) { beginBody = xhtml.indexOf('>',beginBody); while ( xhtml.charAt(beginBody) != '"' ) beginBody++; } else { beginBody = 0; } // Find the end of the body text (ends with double quotes) and extract the body String body; int endPos = xhtml.indexOf("</body>"); if ( endPos > 0) { while ( xhtml.charAt(endPos) != '"' ) endPos--; body = xhtml.substring(beginBody, endPos+1); } else { body = xhtml.substring(beginBody); } // Convert the body to an appropriate format for CSS content String cssContent = convertHTMLToCSSContent(body); // Now see if there are any HTML attributes that need to be returned // as CSS properties. HashMap<String, String> attributes = extractTagAttributes("body", xhtml); if ( attributes.containsKey("bgcolor")) _properties.put("background-color", attributes.get("bgcolor")); return cssContent; } /* * convertPlainTextToCSSContent */ private static String convertPlainTextToCSSContent(String _text) { StringBuilder sb = new StringBuilder(); // First escape all HTML special characters except for the surrounding double quotes _text = '"' + escapeHtmlChars(_text.substring(1,_text.length()-1)) + '"'; // Now escape all newline characters and HTML escaped double quotes for ( int i = 0; i < _text.length(); i++ ) { char ch = _text.charAt(i); switch (ch) { case '\r': case '\n': sb.append("\\A "); // escape sequence for newline if ( (ch == '\r') && (i+1 < _text.length()) && (_text.charAt(i+1) == '\n') ) i++; break; case '&': if ( _text.startsWith(""",i) ) { if ( _text.startsWith("" counter(page) "",i) ) { sb.append("\" counter(page) \""); // put back as unescaped i += "" counter(page) "".length() - 1; // move to the end } else if ( _text.startsWith("" counter(pages) "",i) ) { sb.append("\" counter(pages) \""); // put back as unescaped i += "" counter(pages) "".length() - 1; // move to the end } else { sb.append("\\22 "); // escape sequence for double quote i += """.length() - 1; // move to the end } } else { sb.append(ch); } break; default: sb.append(ch); break; } } return sb.toString(); } /* * convertHTMLToCSSContent */ private static String convertHTMLToCSSContent(String _html) { StringBuilder sb = new StringBuilder(); for ( int i = 0; i < _html.length(); i++ ) { char ch = _html.charAt(i); switch (ch) { case '\r': case '\n': // Do nothing so these characters are removed. break; case '<': int endTag = _html.indexOf('>',i); if ( endTag > i ) { String htmlTag = _html.substring(i, endTag+1); // Currently we only handle the <br> tag. if ( htmlTag.equals("<br />")) sb.append("\\A "); // escape sequence for newline i = endTag; } else { sb.append(ch); } break; default: sb.append(ch); break; } } return sb.toString(); } private String getMargins( cfSession _Session, DocumentSection _section, DocumentSettings _settings ) throws dataNotSupportedException, cfmRunTimeException{ StringBuilder sb = new StringBuilder(); String marginTop = _section.getMarginTop(); if ( _section.getMarginTop() == null ){ marginTop = _settings.getMarginTop(); } if ( marginTop != null ){ sb.append( "margin-top: " ); sb.append( marginTop ); sb.append( _settings.getUnit() ); sb.append( ";\n" ); } String marginBottom = _section.getMarginBottom(); if ( _section.getMarginBottom() == null ){ marginBottom = _settings.getMarginBottom(); } if ( marginBottom != null ){ sb.append( "margin-bottom: " ); sb.append( marginBottom ); sb.append( _settings.getUnit() ); sb.append( ";\n" ); } String marginLeft = _section.getMarginLeft(); if ( _section.getMarginLeft() == null ){ marginLeft = _settings.getMarginLeft(); } if ( marginLeft != null ){ sb.append( "margin-left: " ); sb.append( marginLeft ); sb.append( _settings.getUnit() ); sb.append( ";\n" ); } String marginRight = _section.getMarginRight(); if ( _section.getMarginRight() == null ){ marginRight = _settings.getMarginRight(); } if ( marginRight != null ){ sb.append( "margin-right: " ); sb.append( marginRight ); sb.append( _settings.getUnit() ); sb.append( ";\n" ); } return sb.toString(); } private void appendSectionAttributes( cfSession _Session, DocumentSection _section ) throws cfmRunTimeException{ if ( containsAttribute( "MIMETYPE" ) ) _section.setMimeType( getDynamic( _Session, "MIMETYPE" ).toString() ); if ( containsAttribute( "NAME" ) ) _section.setName( getDynamic( _Session, "NAME" ).toString() ); //TODO: validate values? if ( containsAttribute( "MARGINTOP" ) ) _section.setMarginTop( getDynamic( _Session, "MARGINTOP" ).toString() ); if ( containsAttribute( "MARGINBOTTOM" ) ) _section.setMarginBottom( getDynamic( _Session, "MARGINBOTTOM" ).toString() ); if ( containsAttribute( "MARGINLEFT" ) ) _section.setMarginLeft( getDynamic( _Session, "MARGINLEFT" ).toString() ); if ( containsAttribute( "MARGINRIGHT" ) ) _section.setMarginRight( getDynamic( _Session, "MARGINRIGHT" ).toString() ); _section.setUserAgent( getDynamic( _Session, "USERAGENT" ).getString() ); if ( containsAttribute( "AUTHPASSWORD" ) && containsAttribute( "AUTHUSER" ) ){ _section.setAuthentication( getDynamic( _Session, "AUTHUSER" ).getString(), getDynamic( _Session, "AUTHPASSWORD" ).getString() ); } } private class SessionOutputStream extends java.io.OutputStream{ private cfSession session; private cfmRunTimeException exception; private boolean firstWrite = true; private String saveAsName; SessionOutputStream( cfSession _session, String _saveAsName ){ session = _session; saveAsName = _saveAsName; } public cfmRunTimeException getException(){ return exception; } @Override public void write( byte[] b, int off, int len ) throws IOException { if ( exception == null ){ // only attempt to write further if exception hasn't occurred try { // If this is the first write then set the content type and appropriate headers. // We wait to set these here so that any exceptions that occur before this will // be displayed properly in the browser. if ( firstWrite ){ firstWrite = false; session.setContentType( "application/pdf" ); session.setHeader("Content-Disposition", "inline; filename=" + saveAsName); } session.write( b, off, len ); } catch (cfmRunTimeException e) { exception = e; } } } @Override public void write( byte[] b ) throws IOException { this.write( b, 0, b.length ); } @Override public void write( int arg0 ) throws IOException { this.write( new byte[]{ (byte) arg0 }, 0, 1 ); } } private class NullOutputStream extends java.io.OutputStream{ NullOutputStream(){ } @Override public void write( byte[] b, int off, int len ) throws IOException { } @Override public void write( byte[] b ) throws IOException { } @Override public void write( int arg0 ) throws IOException { } } private class CreationListener extends DefaultPDFCreationListener { private PdfString author; private PdfString title; private PdfString subject; private PdfString keywords; public CreationListener(cfData _author, cfData _title, cfData _subject, cfData _keywords) { if ( _author != null ) author = new PdfString(_author.toString()); if ( _title != null ) title = new PdfString(_title.toString()); if ( _subject != null ) subject = new PdfString(_subject.toString()); if ( _keywords != null ) keywords = new PdfString(_keywords.toString()); } //public void preOpen(ITextRenderer renderer) public void onClose(ITextRenderer renderer) { PdfString creator = new PdfString("OpenBD " + cfEngine.PRODUCT_VERSION + " (" + cfEngine.BUILD_ISSUE + ")"); renderer.getOutputDevice().getWriter().getInfo().put(PdfName.CREATOR,creator); if (author != null) renderer.getOutputDevice().getWriter().getInfo().put(PdfName.AUTHOR,author); if (title != null) renderer.getOutputDevice().getWriter().getInfo().put(PdfName.TITLE,title); if (subject != null) renderer.getOutputDevice().getWriter().getInfo().put(PdfName.SUBJECT,subject); if (keywords != null) renderer.getOutputDevice().getWriter().getInfo().put(PdfName.KEYWORDS,keywords); } } }