/* * Licensed to the Apache Software Foundation (ASF) under one or more * contributor license agreements. See the NOTICE file distributed with * this work for additional information regarding copyright ownership. * The ASF licenses this file to You under the Apache License, Version 2.0 * (the "License"); you may not use this file except in compliance with * the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.apache.cocoon.portal.transformation; import java.io.BufferedInputStream; import java.io.IOException; import java.io.InputStream; import java.io.PrintWriter; import java.io.StringWriter; import java.io.UnsupportedEncodingException; import java.net.HttpURLConnection; import java.net.MalformedURLException; import java.net.URL; import java.util.Enumeration; import java.util.Map; import org.apache.avalon.framework.activity.Disposable; import org.apache.avalon.framework.parameters.ParameterException; import org.apache.avalon.framework.parameters.Parameterizable; import org.apache.avalon.framework.parameters.Parameters; import org.apache.avalon.framework.service.ServiceException; import org.apache.avalon.framework.service.ServiceManager; import org.apache.avalon.framework.service.Serviceable; import org.apache.cocoon.ProcessingException; import org.apache.cocoon.environment.ObjectModelHelper; import org.apache.cocoon.environment.Request; import org.apache.cocoon.environment.SourceResolver; import org.apache.cocoon.portal.Constants; import org.apache.cocoon.portal.PortalService; import org.apache.cocoon.portal.coplet.CopletData; import org.apache.cocoon.portal.coplet.CopletInstanceData; import org.apache.cocoon.portal.profile.ProfileManager; import org.apache.cocoon.portal.util.InputModuleHelper; import org.apache.cocoon.transformation.AbstractTransformer; import org.apache.cocoon.util.NetUtils; import org.apache.cocoon.xml.XMLUtils; import org.apache.cocoon.xml.dom.DOMStreamer; import org.w3c.dom.Document; import org.w3c.dom.Element; import org.w3c.dom.NodeList; import org.w3c.tidy.Configuration; import org.w3c.tidy.Tidy; import org.xml.sax.Attributes; import org.xml.sax.SAXException; /** * This transformer is used to insert the XHTML data from an request * to an external application at the specified element ("envelope-tag" parameter). * Nesessary connection data for the external request like sessionid, cookies, * documentbase, the uri, etc. will be taken from the application coplet instance * data. * @author <a href="mailto:friedrich.klenner@rzb.at">Friedrich Klenner</a> * @author <a href="mailto:gernot.koller@rizit.at">Gernot Koller</a> * * @version CVS $Id$ */ public class ProxyTransformer extends AbstractTransformer implements Serviceable, Disposable, Parameterizable { /** * Parameter for specifying the envelope tag */ public static final String ENVELOPE_TAG_PARAMETER = "envelope-tag"; public static final String PORTALNAME = "cocoon-portal-portalname"; public static final String COPLETID = "cocoon-portal-copletid"; public static final String PROXY_PREFIX = "proxy-"; public static final String COPLET_ID_PARAM = "copletId"; public static final String PORTAL_NAME_PARAM = "portalName"; // Coplet instance data keys public static final String SESSIONTOKEN = "sessiontoken"; public static final String COOKIE = "cookie"; public static final String START_URI = "start-uri"; public static final String LINK = "link"; public static final String DOCUMENT_BASE = "documentbase"; /** * Parameter for specifying the java protocol handler (used for https) */ public static final String PROTOCOL_HANDLER_PARAMETER = "protocol-handler"; /** * The document base uri */ protected String documentBase; /** * The current link to the external application */ protected String link; /** * The default value for the envelope Tag */ protected String defaultEnvelopeTag; /** * This tag will include the external XHMTL */ protected String envelopeTag; /** * The Avalon component manager */ protected ServiceManager manager; /** * The coplet instance data */ protected CopletInstanceData copletInstanceData; /** * The original request to the portal */ protected Request request; /** * The encoding (JTidy constant) if configured */ protected int configuredEncoding; /** * The user agent identification string if confiugured */ protected String userAgent; /** The sitemap parameters */ protected Parameters parameters; /** Helper for resolving input modules. */ protected InputModuleHelper imHelper; /** * @see org.apache.avalon.framework.service.Serviceable#service(org.apache.avalon.framework.service.ServiceManager) */ public void service(ServiceManager manager) throws ServiceException { this.manager = manager; this.imHelper = new InputModuleHelper(manager); } /** * @see org.apache.avalon.framework.activity.Disposable#dispose() */ public void dispose() { if ( this.imHelper != null ) { this.imHelper.dispose(); this.imHelper = null; } } /** * For the proxy transformer the envelope-tag parameter can be specified. * @see org.apache.avalon.framework.parameters.Parameterizable#parameterize(Parameters) */ public void parameterize(Parameters parameters) { this.defaultEnvelopeTag = parameters.getParameter(ENVELOPE_TAG_PARAMETER, null); } /** * @see org.apache.cocoon.sitemap.SitemapModelComponent#setup(SourceResolver, Map, String, Parameters) */ public void setup(SourceResolver resolver, Map objectModel, String src, Parameters parameters) throws ProcessingException, SAXException, IOException { this.parameters = parameters; this.request = ObjectModelHelper.getRequest(objectModel); this.copletInstanceData = getInstanceData(this.manager, objectModel, parameters); final CopletData copletData = this.copletInstanceData.getCopletData(); this.link = (String) this.copletInstanceData.getTemporaryAttribute(LINK); this.documentBase = (String) this.copletInstanceData.getAttribute(DOCUMENT_BASE); if (this.link == null) { final String startURI = (String)copletData.getAttribute(START_URI); this.link = this.imHelper.resolve(startURI); } if (documentBase == null) { this.documentBase = this.link.substring(0, this.link.lastIndexOf('/') + 1); copletInstanceData.setAttribute(DOCUMENT_BASE, this.documentBase); } this.configuredEncoding = encodingConstantFromString((String)copletData.getAttribute("encoding")); this.userAgent = (String)copletData.getAttribute("user-agent"); this.envelopeTag = parameters.getParameter(ENVELOPE_TAG_PARAMETER, this.defaultEnvelopeTag); if (envelopeTag == null) { throw new ProcessingException("Can not initialize ProxyTransformer - sitemap parameter 'envelope-tag' missing"); } } /** * @see org.apache.avalon.excalibur.pool.Recyclable#recycle() */ public void recycle() { super.recycle(); this.envelopeTag = null; this.userAgent = null; this.documentBase = null; this.link = null; this.request = null; this.parameters = null; this.copletInstanceData = null; } /** * @see org.xml.sax.ContentHandler#startElement(String, String, String, Attributes) */ public void startElement( String uri, String name, String raw, Attributes attributes) throws SAXException { super.startElement(uri, name, raw, attributes); if (name.equalsIgnoreCase(this.envelopeTag)) { //super.startElement(uri, name, raw, attributes); processRequest(); //super.endElement(uri, name, raw); } } /** * Processes the request to the external application */ protected void processRequest() throws SAXException { try { String remoteURI = null; try { remoteURI = resolveURI(link, documentBase); } catch (MalformedURLException ex) { throw new SAXException(ex); } boolean firstparameter = true; boolean post = ("POST".equals(request.getMethod())); int pos = remoteURI.indexOf('?'); final StringBuffer query = new StringBuffer(); if ( pos != -1 ) { if ( !post ) { query.append('?'); } query.append(remoteURI.substring(pos+1)); firstparameter = true; remoteURI = remoteURI.substring(0, pos); } // append all parameters of the current request, except those where // the name of the request parameter starts with "cocoon-portal-" final Enumeration enumeration = request.getParameterNames(); while (enumeration.hasMoreElements()) { String paramName = (String) enumeration.nextElement(); if (!paramName.startsWith("cocoon-portal-")) { String[] paramValues = request.getParameterValues(paramName); for (int i = 0; i < paramValues.length; i++) { firstparameter = this.appendParameter(query, firstparameter, post, paramName, paramValues[i]); } } } // now append parameters from the sitemap - if any final String[] names = this.parameters.getNames(); for(int i=0; i<names.length; i++) { if ( names[i].startsWith("add:") ) { final String value = this.parameters.getParameter(names[i]); if ( value != null && value.trim().length() > 0 ) { final String pName = names[i].substring(4); firstparameter = this.appendParameter(query, firstparameter, post, pName, value.trim()); } } } Document result = null; try { do { if ( this.getLogger().isDebugEnabled() ) { this.getLogger().debug("Invoking '" + remoteURI + query.toString() +"', post="+post); } HttpURLConnection connection = connect(request, remoteURI, query.toString(), post); remoteURI = checkForRedirect(connection, documentBase); if (remoteURI == null) { result = readXML(connection); remoteURI = checkForRedirect(result, documentBase); } } while (remoteURI != null); } catch (IOException ex) { throw new SAXException( "Failed to retrieve remoteURI " + remoteURI, ex); } XMLUtils.stripDuplicateAttributes(result, null); DOMStreamer streamer = new DOMStreamer(); streamer.setContentHandler(contentHandler); streamer.stream(result.getDocumentElement()); } catch (SAXException se) { throw se; } catch (Exception ex) { throw new SAXException(ex); } } protected boolean appendParameter(StringBuffer buffer, boolean firstparameter, boolean post, String name, String value) throws UnsupportedEncodingException { if (firstparameter) { if (!post) { buffer.append('?'); } firstparameter = false; } else { buffer.append('&'); } buffer.append(NetUtils.encode(name, "utf-8")); buffer.append('='); buffer.append(NetUtils.encode(value, "utf-8")); return firstparameter; } /** * Check the http status code of the http response to detect any redirects. * @param connection The HttpURLConnection * @param documentBase The current documentBase (needed for relative redirects) * @return the redirected URL or null if no redirects are detected. * @throws IOException if exceptions occure while analysing the response */ protected String checkForRedirect( HttpURLConnection connection, String documentBase) throws IOException { if (connection.getResponseCode() == HttpURLConnection.HTTP_MOVED_PERM || connection.getResponseCode() == HttpURLConnection.HTTP_MOVED_TEMP) { String newURI = (connection.getHeaderField("location")); int index_semikolon = newURI.indexOf(";"); int index_question = newURI.indexOf("?"); if ((index_semikolon > -1)) { String sessionToken = newURI.substring( index_semikolon + 1, (index_question == -1 ? newURI.length() : index_question)); this.copletInstanceData.getPersistentAspectData().put( SESSIONTOKEN, sessionToken); } newURI = resolveURI(newURI, documentBase); return newURI; } return null; } /** * Analyses the XHTML response document for redirects in <meta http-equiv="refresh"> elements. * @param doc The W3C DOM document containing the XHTML response * @param documentBase The current document base (needed for relative redirects) * @return String the redirected URL or null if no redirects are detected. * @throws MalformedURLException if the redirect uri is malformed. */ protected String checkForRedirect(Document doc, String documentBase) throws MalformedURLException { Element htmlElement = doc.getDocumentElement(); NodeList headList = htmlElement.getElementsByTagName("head"); if (headList.getLength() <= 0) { return null; } Element headElement = (Element) headList.item(0); NodeList metaList = headElement.getElementsByTagName("meta"); for (int i = 0; i < metaList.getLength(); i++) { Element metaElement = (Element) metaList.item(i); String httpEquiv = metaElement.getAttribute("http-equiv"); if ("refresh".equalsIgnoreCase(httpEquiv)) { String content = metaElement.getAttribute("content"); if (content != null) { String time = content.substring(0, content.indexOf(';')); try { if (Integer.parseInt(time) > 10) { getLogger().warn( "Redirects with refresh time longer than 10 seconds (" + time + " seconds) will be ignored!"); return null; } } catch (NumberFormatException ex) { getLogger().warn( "Failed to convert refresh time from redirect to integer: " + time); return null; } String newURI = content.substring(content.indexOf('=') + 1); int index_semikolon = newURI.indexOf(";"); int index_question = newURI.indexOf("?"); if ((index_semikolon > -1)) { String sessionToken = newURI.substring( index_semikolon + 1, (index_question == -1 ? newURI.length() : index_question)); this.copletInstanceData.getPersistentAspectData().put( SESSIONTOKEN, sessionToken); } newURI = resolveURI(newURI, documentBase); return newURI; } } } return null; } /** * Reads the HTML document from given connection and returns a correct W3C DOM XHTML document * @param connection hte HttpURLConnection to read from * @return the result as valid W3C DOM XHTML document */ protected Document readXML(HttpURLConnection connection) throws SAXException { try { int charEncoding = configuredEncoding; String contentType = connection.getHeaderField("Content-Type"); int begin = contentType.indexOf("charset="); int end = -1; if (begin > -1) { begin += "charset=".length(); end = contentType.indexOf(';', begin); if (end == -1) { end = contentType.length(); } String charset = contentType.substring(begin, end); charEncoding = encodingConstantFromString(charset); } InputStream stream = connection.getInputStream(); // Setup an instance of Tidy. Tidy tidy = new Tidy(); tidy.setXmlOut(true); tidy.setCharEncoding(charEncoding); tidy.setXHTML(true); //Set Jtidy warnings on-off tidy.setShowWarnings(this.getLogger().isWarnEnabled()); //Set Jtidy final result summary on-off tidy.setQuiet(!this.getLogger().isInfoEnabled()); //Set Jtidy infos to a String (will be logged) instead of System.out StringWriter stringWriter = new StringWriter(); //FIXME ?? PrintWriter errorWriter = new PrintWriter(stringWriter); tidy.setErrout(errorWriter); // Extract the document using JTidy and stream it. Document doc = tidy.parseDOM(new BufferedInputStream(stream), null); errorWriter.flush(); errorWriter.close(); return doc; } catch (Exception ex) { throw new SAXException(ex); } } /** * Helper method to convert the HTTP encoding String to JTidy encoding constants. * @param encoding the HTTP encoding String * @return the corresponding JTidy constant. */ private int encodingConstantFromString(String encoding) { if ("ISO8859_1".equalsIgnoreCase(encoding)) { return Configuration.LATIN1; } else if ("UTF-8".equalsIgnoreCase(encoding)) { return Configuration.UTF8; } else { return Configuration.LATIN1; } } /** * Establish the HttpURLConnection to the given uri. * User-Agent, Accept-Language and Encoding headers will be copied from the original * request, if no other headers are specified. * @param request the original request * @param uri the remote uri * @param query the remote query string * @param post true if request method was POST * @return the established HttpURLConnection * @throws IOException on any exception */ protected HttpURLConnection connect( Request request, String uri, String query, boolean post) throws IOException { String cookie = (String) copletInstanceData.getAttribute(COOKIE); if (!post) { uri = uri + query; } URL url = new URL(uri); HttpURLConnection connection = (HttpURLConnection) url.openConnection(); connection.setInstanceFollowRedirects(false); connection.setRequestMethod(request.getMethod()); connection.setRequestProperty( "User-Agent", (userAgent != null) ? userAgent : request.getHeader("User-Agent")); connection.setRequestProperty( "Accept-Language", request.getHeader("Accept-Language")); if (cookie != null) { connection.setRequestProperty(COOKIE, cookie); } if (post) { connection.setDoOutput(true); connection.setRequestProperty( "Content-Type", "application/x-www-form-urlencoded"); connection.setRequestProperty( "Content-Length", String.valueOf(query.length())); } connection.connect(); if (post) { PrintWriter out = new PrintWriter(connection.getOutputStream()); out.print(query); out.close(); } copletInstanceData.setAttribute( COOKIE, connection.getHeaderField(COOKIE)); documentBase = uri.substring(0, uri.lastIndexOf('/') + 1); copletInstanceData.setAttribute(DOCUMENT_BASE, documentBase); return connection; } /** * Resolve the possibly relative uri to an absolue uri based on given document base. * @param uri the uri to resolve * @param documentBase the current document base * @return returns an absolute URI based on document base (e.g. http://mydomain.com/some/file.html) * @throws MalformedURLException if uri or document base is malformed. */ public static String resolveURI(String uri, String documentBase) throws MalformedURLException { if (uri == null) { throw new IllegalArgumentException("URI to be resolved must not be null!"); } if (uri.indexOf("://") > -1) { return uri; } if (documentBase == null) { throw new IllegalArgumentException("Documentbase String must not be null!"); } //cut ./ from uri if (uri.startsWith("./")) { uri = uri.substring(2); } URL documentBaseURL = new URL(documentBase); //absolute uri if (uri.startsWith("/")) { return documentBaseURL.getProtocol() + "://" + documentBaseURL.getAuthority() + uri; } return documentBaseURL.toExternalForm() + uri; } public static CopletInstanceData getInstanceData(ServiceManager manager, String copletID, String portalName) throws ProcessingException { PortalService portalService = null; try { portalService = (PortalService) manager.lookup(PortalService.ROLE); ProfileManager profileManager = portalService.getComponentManager().getProfileManager(); CopletInstanceData data = profileManager.getCopletInstanceData(copletID); return data; } catch (ServiceException e) { throw new ProcessingException("Error getting portal service.", e); } finally { manager.release(portalService); } } /** * Method getInstanceData. * @param manager * @param objectModel * @param parameters * @return CopletInstanceData * @throws ProcessingException */ public static CopletInstanceData getInstanceData(ServiceManager manager, Map objectModel, Parameters parameters) throws ProcessingException { PortalService portalService = null; try { portalService = (PortalService) manager.lookup(PortalService.ROLE); // determine coplet id String copletId = null; Map context = (Map) objectModel.get(ObjectModelHelper.PARENT_CONTEXT); if (context != null) { copletId = (String) context.get(Constants.COPLET_ID_KEY); if (copletId == null) { throw new ProcessingException("copletId must be passed as parameter or in the object model within the parent context."); } } else { try { copletId = parameters.getParameter(COPLET_ID_PARAM); } catch (ParameterException e) { throw new ProcessingException("copletId and portalName must be passed as parameter or in the object model within the parent context."); } } return portalService.getComponentManager().getProfileManager().getCopletInstanceData(copletId); } catch (ServiceException e) { throw new ProcessingException("Error getting portal service.", e); } finally { manager.release(portalService); } } }