/*!
* This program is free software; you can redistribute it and/or modify it under the
* terms of the GNU Lesser General Public License, version 2.1 as published by the Free Software
* Foundation.
*
* You should have received a copy of the GNU Lesser General Public License along with this
* program; if not, you can obtain a copy at http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html
* or from the Free Software Foundation, Inc.,
* 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
*
* This program 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 Lesser General Public License for more details.
*
* Copyright (c) 2002-2017 Pentaho Corporation.. All rights reserved.
*/
package org.pentaho.platform.web.http.filters;
import org.apache.commons.lang.StringEscapeUtils;
import org.apache.commons.lang.StringUtils;
import org.owasp.encoder.Encode;
import org.pentaho.platform.api.engine.IApplicationContext;
import org.pentaho.platform.api.engine.ICacheManager;
import org.pentaho.platform.api.engine.IPentahoRequestContext;
import org.pentaho.platform.api.engine.IPluginManager;
import org.pentaho.platform.api.engine.IPentahoSession;
import org.pentaho.platform.api.usersettings.IUserSettingService;
import org.pentaho.platform.engine.core.system.PentahoRequestContextHolder;
import org.pentaho.platform.engine.core.system.PentahoSessionHolder;
import org.pentaho.platform.engine.core.system.PentahoSystem;
import org.pentaho.platform.repository2.ClientRepositoryPaths;
import org.pentaho.platform.repository2.unified.jcr.JcrRepositoryFileUtils;
import org.pentaho.platform.util.messages.LocaleHelper;
import javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.FilterConfig;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.OutputStream;
import java.util.Iterator;
import java.util.List;
import java.util.Locale;
import java.util.Map;
/**
* If the request is searching for a webcontext.js, it writes out the content of the webcontext.js
*
* @author Ramaiz Mansoor
*
*/
public class PentahoWebContextFilter implements Filter {
public static final String WEB_CONTEXT_JS = "webcontext.js"; //$NON-NLS-1$
static final String FILTER_APPLIED = "__pentaho_web_context_filter_applied"; //$NON-NLS-1$
static final String initialComment =
"/** webcontext.js is created by a PentahoWebContextFilter. This filter searches for an " + //$NON-NLS-1$
"incoming URI having \"webcontext.js\" in it. If it finds that, "
+ "it write CONTEXT_PATH and FULLY_QUALIFIED_SERVER_URL"
+ //$NON-NLS-1$
" and it values from the servlet request to this js **/ \n\n\n"; //$NON-NLS-1$
static final byte[] initialCommentBytes = initialComment.getBytes();
String fullyQualifiedUrl = null;
String serverProtocol = null;
private static final String JS = ".js"; //$NON-NLS-1$
private static final String CSS = ".css"; //$NON-NLS-1$
private static final String CONTEXT = "context"; //$NON-NLS-1$
private static final String GLOBAL = "global"; //$NON-NLS-1$
private static final String REQUIRE_JS = "requirejs"; //$NON-NLS-1$
// Changed to not do so much work for every request
private static final ThreadLocal<byte[]> THREAD_LOCAL_REQUIRE_SCRIPT = new ThreadLocal<byte[]>();
protected static ICacheManager cache = PentahoSystem.getCacheManager( null );
public void destroy() {
// TODO Auto-generated method stub
}
protected void close( OutputStream out ) {
try {
out.close();
} catch ( IOException e ) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
public void doFilter( ServletRequest request, ServletResponse response, FilterChain chain ) throws IOException,
ServletException {
HttpServletRequest httpRequest = (HttpServletRequest) request;
HttpServletResponse httpResponse = (HttpServletResponse) response;
String requestStr = httpRequest.getRequestURI();
if ( requestStr != null && requestStr.endsWith( WEB_CONTEXT_JS )
&& httpRequest.getAttribute( FILTER_APPLIED ) == null ) {
httpRequest.setAttribute( FILTER_APPLIED, Boolean.TRUE );
// split out a fully qualified url, guaranteed to have a trailing slash
IPentahoRequestContext requestContext = getRequestContext();
String contextPath = requestContext.getContextPath();
final boolean shouldUseFullyQualifiedUrl = shouldUseFullyQualifiedUrl( httpRequest );
if ( shouldUseFullyQualifiedUrl ) {
contextPath = getFullyQualifiedServerURL();
}
try {
response.setContentType( "text/javascript" );
OutputStream out = response.getOutputStream();
out.write( initialCommentBytes );
String webContext = "var CONTEXT_PATH = '" + contextPath + "';\n\n";
out.write( webContext.getBytes() );
out.write( fullyQualifiedUrl.getBytes() );
out.write( serverProtocol.getBytes() );
// Compute the effective locale and set it in the global scope. Also provide it as a module if the RequireJs
// system is available.
Locale effectiveLocale = LocaleHelper.getLocale();
if ( !StringUtils.isEmpty( request.getParameter( "locale" ) ) ) {
effectiveLocale = new Locale( request.getParameter( "locale" ) );
}
// context name variable
String contextName = request.getParameter( CONTEXT );
printContextName( contextName, out );
// active_theme variable
printActiveTheme( httpRequest, out );
// setup a RequireJS config object for plugins to extend
printRequireJsCfgStart( out );
// Let all plugins contribute to the RequireJS config
printResourcesForContext( REQUIRE_JS, out, httpRequest, false );
byte[] requireScriptBytes = THREAD_LOCAL_REQUIRE_SCRIPT.get();
if ( requireScriptBytes == null ) {
String requireJsLocation = "content/common-ui/resources/web/require.js";
String requireJsConfigLocation = "content/common-ui/resources/web/require-cfg.js";
String requireScript =
"document.write(\"<script type='text/javascript' src='\" + CONTEXT_PATH + \""
+ requireJsLocation + "'></scr\"+\"ipt>\");\n"
+ "document.write(\"<script type=\'text/javascript\' src='\" + CONTEXT_PATH + \""
+ requireJsConfigLocation + "'></scr\"+\"ipt>\");\n";
requireScriptBytes = requireScript.getBytes();
THREAD_LOCAL_REQUIRE_SCRIPT.set( requireScriptBytes );
}
out.write( requireScriptBytes );
printSessionName( out );
printLocale( effectiveLocale, out );
printHomeFolder( out );
printReservedChars( out );
printReservedCharsDisplay( out );
printReservedRegexPattern( out );
boolean noOsgiRequireConfig = "true".equals( request.getParameter( "noOsgiRequireConfig" ) );
if ( !noOsgiRequireConfig && !"anonymousUser".equals( getSession().getName() ) ) {
final String useFullyQualifiedUrlParameter = httpRequest.getParameter( "fullyQualifiedUrl" );
out.write( ( "document.write(\"<script type='text/javascript' src='\" + CONTEXT_PATH + \""
+ "osgi/requirejs-manager/js/require-init.js?requirejs=false"
+ ( useFullyQualifiedUrlParameter != null ? "&fullyQualifiedUrl=" + useFullyQualifiedUrlParameter : "" )
+ "'></scr\"+\"ipt>\");\n" ).getBytes( "UTF-8" ) );
}
boolean requireJsOnly = "true".equals( request.getParameter( "requireJsOnly" ) );
if ( !requireJsOnly ) {
// print global resources defined in plugins
printResourcesForContext( GLOBAL, out, httpRequest, false );
// print out external-resources defined in plugins if a context has been passed in
boolean cssOnly = "true".equals( request.getParameter( "cssOnly" ) );
if ( StringUtils.isNotEmpty( contextName ) ) {
printResourcesForContext( contextName, out, httpRequest, cssOnly );
}
}
// Any subclass can add more information to webcontext.js
addCustomInfo( out );
out.close();
return;
} finally {
httpRequest.removeAttribute( FILTER_APPLIED );
}
} else {
chain.doFilter( httpRequest, httpResponse );
return;
}
}
private boolean shouldUseFullyQualifiedUrl( HttpServletRequest httpRequest ) {
final String useFullyQualifiedUrlParameter = httpRequest.getParameter( "fullyQualifiedUrl" );
if ( useFullyQualifiedUrlParameter != null ) {
return "true".equals( useFullyQualifiedUrlParameter );
} else {
final String referer = httpRequest.getHeader( "referer" );
final String serverAddress = httpRequest.getScheme() + "://" + httpRequest.getServerName() + ":" + httpRequest.getServerPort();
return referer != null && !referer.startsWith( serverAddress );
}
}
private void printHomeFolder( OutputStream out ) throws IOException {
StringBuilder sb = new StringBuilder( "// Providing home folder location for UI defaults\n" );
if ( getSession() != null ) {
String homePath = ClientRepositoryPaths.getUserHomeFolderPath( StringEscapeUtils
.escapeJavaScript( getSession().getName() ) );
sb.append( "var HOME_FOLDER = '" + homePath + "';\n" ); // Global variable
} else {
sb.append( "var HOME_FOLDER = null;\n" ); // Global variable
}
out.write( sb.toString().getBytes() );
}
private void printReservedChars( OutputStream out ) throws IOException {
StringBuilder sb = new StringBuilder();
for ( char c : JcrRepositoryFileUtils.getReservedChars() ) {
sb.append( c );
}
String scriptLine =
"var RESERVED_CHARS = \""
+ StringEscapeUtils.escapeJavaScript( sb.toString() )
+ "\";\n";
out.write( scriptLine.getBytes() );
}
private void printReservedCharsDisplay( OutputStream out ) throws IOException {
List<Character> reservedCharacters = JcrRepositoryFileUtils.getReservedChars();
StringBuffer sb = new StringBuffer();
for ( int i = 0; i < reservedCharacters.size(); i++ ) {
if ( reservedCharacters.get( i ) >= 0x07 && reservedCharacters.get( i ) <= 0x0d ) {
sb.append( StringEscapeUtils.escapeJava( "" + reservedCharacters.get( i ) ) );
} else {
sb.append( reservedCharacters.get( i ) );
}
if ( i + 1 < reservedCharacters.size() ) {
sb.append( ", " );
}
}
String scriptLine =
"var RESERVED_CHARS_DISPLAY = \""
+ StringEscapeUtils.escapeJavaScript( sb.toString() )
+ "\";\n";
out.write( scriptLine.getBytes() );
}
private void printReservedRegexPattern( OutputStream out ) throws IOException {
String scriptLine = "var RESERVED_CHARS_REGEX_PATTERN = /" + makeReservedCharPattern() + "/;\n";
out.write( scriptLine.getBytes() );
}
private static String makeReservedCharPattern() {
// escape all reserved characters as they may have special meaning to regex engine
StringBuilder buf = new StringBuilder();
buf.append( ".*[" ); //$NON-NLS-1$
for ( Character ch : JcrRepositoryFileUtils.getReservedChars() ) {
buf.append( StringEscapeUtils.escapeJavaScript( ch.toString() ) );
}
buf.append( "]+.*" ); //$NON-NLS-1$
return buf.toString();
}
private void printSessionName( OutputStream out ) throws IOException {
StringBuilder sb = new StringBuilder( "// Providing name for session\n" );
if ( getSession() == null ) {
sb.append( "var SESSION_NAME = null;\n" ); // Global variable
} else {
sb.append( "var SESSION_NAME = '"
+ StringEscapeUtils.escapeJavaScript( getSession().getName() ) + "';\n" ); // Global variable
}
out.write( sb.toString().getBytes() );
}
private void printLocale( Locale effectiveLocale, OutputStream out ) throws IOException {
StringBuilder sb =
new StringBuilder( "// Providing computed Locale for session\n" ).append(
"var SESSION_LOCALE = '" + effectiveLocale.toString() + "';\n" ) // Global variable
// If RequireJs is available, supply a module
.append(
"if(typeof(pen) != 'undefined' && pen.define){pen.define('Locale', {locale:'"
+ effectiveLocale.toString() + "'})};" );
out.write( sb.toString().getBytes() );
}
private void printContextName( String contextName, OutputStream out ) throws IOException {
StringBuilder sb = new StringBuilder( "// Providing name for context\n" );
sb.append( "var PENTAHO_CONTEXT_NAME = '"
+ StringEscapeUtils.escapeJavaScript( contextName ) + "';\n\n" ); // Global variable
out.write( sb.toString().getBytes() );
}
private void printActiveTheme( HttpServletRequest request, OutputStream out ) throws IOException {
// NOTE: this code should be kept in sync with that of ThemeServlet.java
StringBuilder sb = new StringBuilder( "// Providing active theme\n" );
IPentahoSession session = getSession();
String activeTheme = (String) session.getAttribute( "pentaho-user-theme" );
String ua = request.getHeader( "User-Agent" );
// check if we're coming from a mobile device, if so, lock to system default (crystal)
if ( !StringUtils.isEmpty( ua ) && ua.matches( ".*(?i)(iPad|iPod|iPhone|Android).*" ) ) {
activeTheme = PentahoSystem.getSystemSetting( "default-theme", "crystal" );
}
if ( activeTheme == null ) {
IUserSettingService settingsService = PentahoSystem.get( IUserSettingService.class, session );
try {
activeTheme = settingsService.getUserSetting( "pentaho-user-theme", null ).getSettingValue();
} catch ( Exception ignored ) {
// the user settings service is not valid in the agile-bi deployment of the server
}
if ( activeTheme == null ) {
activeTheme = PentahoSystem.getSystemSetting( "default-theme", "crystal" );
}
}
sb.append( "var active_theme = '"
+ StringEscapeUtils.escapeJavaScript( activeTheme )
+ "';\n\n" ); // Global variable
out.write( sb.toString().getBytes() );
}
private void printResourcesForContext( String contextName, OutputStream out, HttpServletRequest request,
boolean printCssOnly ) throws IOException {
IPluginManager pluginManager = getPluginManager();
HttpServletRequest req = ( (HttpServletRequest) request );
String reqStr = "";
Map paramMap = req.getParameterMap();
// Fix for BISERVER-7613, BISERVER-7614, BISERVER-7615
// Make sure that parameters in the URL are encoded for Javascript safety since they'll be
// added to Javascript fragments that get executed.
if ( paramMap.size() > 0 ) {
StringBuilder sb = new StringBuilder();
Map.Entry<String, String[]> me = null;
char sep = '?'; // first separator is '?'
Iterator<Map.Entry<String, String[]>> it = paramMap.entrySet().iterator();
int i;
while ( it.hasNext() ) {
me = it.next();
for ( i = 0; i < me.getValue().length; i++ ) {
sb.append( sep ).append( Encode.forJavaScript( me.getKey().toString() ) ).append( "=" ).append(
Encode.forJavaScript( me.getValue()[i] ) );
}
if ( sep == '?' ) {
sep = '&'; // change the separator
}
}
reqStr = sb.toString(); // get the request string.
}
List<String> externalResources = pluginManager.getExternalResourcesForContext( contextName );
out.write( ( "<!-- Injecting web resources defined in by plugins as external-resources for: "
+ Encode.forHtml(
contextName ) + "-->\n" ).getBytes() ); //$NON-NLS-1$ //$NON-NLS-2$
if ( externalResources != null ) {
for ( String res : externalResources ) {
if ( res == null ) {
continue;
}
if ( res.endsWith( JS ) && !printCssOnly ) {
out.write( ( "document.write(\"<script language='javascript' type='text/javascript' src='\" + CONTEXT_PATH + \"" + res.trim() + reqStr + "'></scr\"+\"ipt>\");\n" //$NON-NLS-1$ //$NON-NLS-2$
).getBytes() );
} else if ( res.endsWith( CSS ) ) {
out.write( ( "document.write(\"<link rel='stylesheet' type='text/css' href='\" + CONTEXT_PATH + \"" + res.trim() + reqStr + "'/>\");\n" //$NON-NLS-1$ //$NON-NLS-2$
).getBytes() );
}
}
}
}
protected void printRequireJsCfgStart( OutputStream out ) throws IOException {
Integer waitTime = null;
if ( cache != null ) {
waitTime = (Integer) cache.getFromGlobalCache( PentahoSystem.WAIT_SECONDS );
}
if ( waitTime == null ) {
try {
waitTime = Integer.valueOf( PentahoSystem.getSystemSetting( PentahoSystem.WAIT_SECONDS, "30" ) );
} catch ( NumberFormatException e ) {
waitTime = 30;
}
if ( cache != null ) {
cache.putInGlobalCache( PentahoSystem.WAIT_SECONDS, waitTime );
}
}
String requireJsCfgStart = "var requireCfg = {waitSeconds: " + waitTime
+ ", paths: {}, shim: {}, map: {\"*\": {}}, bundles: {}, config: {\"pentaho/service\": {}}, packages: []};\n";
out.write( requireJsCfgStart.getBytes() );
}
protected void addCustomInfo( OutputStream out ) throws IOException {
}
public void init( FilterConfig filterConfig ) throws ServletException {
String fullyQualifiedServerURL = getFullyQualifiedServerURL();
String serverProtocolValue;
if ( fullyQualifiedServerURL.startsWith( "http" ) ) {
serverProtocolValue = fullyQualifiedServerURL.substring( 0, fullyQualifiedServerURL.indexOf( ":" ) );
} else {
serverProtocolValue = "http";
}
fullyQualifiedUrl = "var FULL_QUALIFIED_URL = '" + fullyQualifiedServerURL + "';\n\n"; //$NON-NLS-1$ //$NON-NLS-2$
serverProtocol = "var SERVER_PROTOCOL = '" + serverProtocolValue + "';\n\n"; //$NON-NLS-1$ //$NON-NLS-2$
}
private String getFullyQualifiedServerURL() {
// split out a fully qualified url, guaranteed to have a trailing slash
String fullyQualifiedServerURL = getApplicationContext().getFullyQualifiedServerURL();
if ( !fullyQualifiedServerURL.endsWith( "/" ) ) {
fullyQualifiedServerURL += "/";
}
return fullyQualifiedServerURL;
}
IApplicationContext getApplicationContext() {
return PentahoSystem.getApplicationContext();
}
IPentahoRequestContext getRequestContext() {
return PentahoRequestContextHolder.getRequestContext();
}
IPentahoSession getSession() {
return PentahoSessionHolder.getSession();
}
IPluginManager getPluginManager() {
return PentahoSystem.get( IPluginManager.class );
}
}