/* * Copyright 2000-2013 Enonic AS * http://www.enonic.com/license */ package com.enonic.cms.web.portal.services; import java.io.IOException; import java.io.UnsupportedEncodingException; import java.net.MalformedURLException; import java.net.URL; import java.rmi.RemoteException; import java.util.ArrayList; import java.util.Enumeration; import java.util.Iterator; import java.util.List; import java.util.Map; import javax.mail.MessagingException; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import javax.servlet.http.HttpSession; import org.apache.commons.fileupload.FileItem; import org.apache.commons.fileupload.FileUpload; import org.apache.commons.fileupload.FileUploadBase; import org.apache.commons.fileupload.FileUploadException; import org.apache.commons.fileupload.disk.DiskFileItemFactory; import org.apache.commons.fileupload.servlet.ServletRequestContext; import org.apache.commons.lang.StringUtils; import org.joda.time.format.DateTimeFormat; import org.joda.time.format.DateTimeFormatter; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; import org.springframework.web.servlet.ModelAndView; import com.google.common.base.Splitter; import com.google.common.collect.ImmutableList; import com.google.common.collect.Iterables; import com.enonic.esl.containers.ExtendedMap; import com.enonic.esl.containers.MultiValueMap; import com.enonic.vertical.adminweb.VerticalAdminLogger; import com.enonic.vertical.engine.VerticalCreateException; import com.enonic.vertical.engine.VerticalEngineException; import com.enonic.vertical.engine.VerticalRemoveException; import com.enonic.vertical.engine.VerticalSecurityException; import com.enonic.cms.framework.util.UrlPathDecoder; import com.enonic.cms.core.Attribute; import com.enonic.cms.core.captcha.CaptchaService; import com.enonic.cms.core.content.ContentParserService; import com.enonic.cms.core.content.ContentService; import com.enonic.cms.core.content.access.ContentAccessException; import com.enonic.cms.core.content.category.CategoryAccessException; import com.enonic.cms.core.mail.SendMailService; import com.enonic.cms.core.portal.VerticalSession; import com.enonic.cms.core.portal.cache.PageCacheService; import com.enonic.cms.core.portal.httpservices.HttpServicesException; import com.enonic.cms.core.security.SecurityService; import com.enonic.cms.core.security.userstore.UserStoreService; import com.enonic.cms.core.service.UserServicesService; import com.enonic.cms.core.structure.SiteContext; import com.enonic.cms.core.structure.SiteKey; import com.enonic.cms.core.structure.SitePath; import com.enonic.cms.core.structure.SiteService; import com.enonic.cms.store.dao.CategoryDao; import com.enonic.cms.store.dao.ContentDao; import com.enonic.cms.store.dao.SiteDao; import com.enonic.cms.web.portal.PortalSitePathResolver; import com.enonic.cms.web.portal.PortalWebContext; import com.enonic.cms.web.portal.SiteRedirectHelper; public abstract class ServicesProcessorBase implements ServicesProcessor { private final String handlerName; // fatal errors public final static int ERR_OPERATION_BACKEND = 504; // http 500 Internal Server Error public final static int ERR_SECURITY_EXCEPTION = 506; // http 401 Unautorized or 403 Forbidden, depending on anonymous // general errors public final static int ERR_PARAMETERS_MISSING = 400; // http 400 Bad Request public final static int ERR_PARAMETERS_INVALID = 401; // http 400 Bad Request public final static int ERR_EMAIL_SEND_FAILED = 402; // http 500 Internal Server Error public final static int ERR_INVALID_CAPTCHA = 405; // http 400 Bad Request public final static int ERR_CONTENT_NOT_FOUND = 406; // http 400 Bad Request // HTTP response status codes in use with http services: public final static int HTTP_STATUS_BAD_REQUEST = HttpServletResponse.SC_BAD_REQUEST; public final static int HTTP_STATUS_UNAUTHORIZED = HttpServletResponse.SC_UNAUTHORIZED; public final static int HTTP_STATUS_FORBIDDEN = HttpServletResponse.SC_FORBIDDEN; public final static int HTTP_STATUS_INTERNAL_SERVER_ERROR = HttpServletResponse.SC_INTERNAL_SERVER_ERROR; protected static final DateTimeFormatter DATE_FORMAT_FROM = DateTimeFormat.forPattern( "dd.MM.yyyy" ); protected static final DateTimeFormatter DATE_FORMAT_TO = DateTimeFormat.forPattern( "yyyy-MM-dd" ); private final FileUploadBase fileUpload; protected CaptchaService captchaService; private UserServicesRedirectUrlResolver userServicesRedirectUrlResolver; private UserServicesAccessManager userServicesAccessManager; private SiteService siteService; private PortalSitePathResolver sitePathResolver; private SiteRedirectHelper siteRedirectHelper; protected SiteDao siteDao; protected CategoryDao categoryDao; protected ContentDao contentDao; protected SecurityService securityService; protected UserStoreService userStoreService; protected SendMailService sendMailService; protected ContentParserService contentParserService; protected ContentService contentService; protected PageCacheService pageCacheService; protected boolean transliterate; private UserServicesService userServicesService; private ImmutableList<String> allowedRedirectDomains; public ServicesProcessorBase( final String handlerName ) { this.handlerName = handlerName; fileUpload = new FileUpload( new DiskFileItemFactory() ); fileUpload.setHeaderEncoding( "UTF-8" ); this.allowedRedirectDomains = ImmutableList.of( "*" ); } public final String getHandlerName() { return this.handlerName; } public final void handle( final PortalWebContext context ) throws Exception { final HttpServletRequest request = context.getRequest(); final HttpServletResponse response = context.getResponse(); handleRequest( request, response ); } @Autowired public void setUserServicesRedirectHelper( UserServicesRedirectUrlResolver value ) { this.userServicesRedirectUrlResolver = value; } @Autowired public void setCaptchaService( CaptchaService service ) { captchaService = service; } @Autowired public void setUserServicesService( UserServicesService userServicesService ) { this.userServicesService = userServicesService; } @Autowired public void setPageCacheService( PageCacheService value ) { this.pageCacheService = value; } @Autowired public void setContentDao( ContentDao contentDao ) { this.contentDao = contentDao; } @Autowired public void setContentParserService( ContentParserService contentParserService ) { this.contentParserService = contentParserService; } @Autowired public void setContentService( ContentService contentService ) { this.contentService = contentService; } @Autowired public void setSiteDao( SiteDao siteDao ) { this.siteDao = siteDao; } @Autowired public void setSiteRedirectHelper( SiteRedirectHelper value ) { this.siteRedirectHelper = value; } @Autowired public void setSiteService( SiteService value ) { this.siteService = value; } @Autowired public void setSitePathResolver( PortalSitePathResolver value ) { this.sitePathResolver = value; } @Autowired public void setCategoryDao( CategoryDao categoryDao ) { this.categoryDao = categoryDao; } @Autowired public void setSecurityService( SecurityService value ) { this.securityService = value; } @Autowired public void setUserStoreService( UserStoreService userStoreService ) { this.userStoreService = userStoreService; } @Autowired public void setSendMailService( SendMailService sendMailService ) { this.sendMailService = sendMailService; } protected void handlerCreate( HttpServletRequest request, HttpServletResponse response, HttpSession session, ExtendedMap formItems, UserServicesService userServices, SiteKey siteKey ) throws VerticalUserServicesException, VerticalCreateException, VerticalSecurityException, IOException, MessagingException { String message = "OperationWrapper CREATE not implemented."; VerticalUserServicesLogger.error( message ); } protected void handlerRemove( HttpServletRequest request, HttpServletResponse response, HttpSession session, ExtendedMap formItems, UserServicesService userServices, SiteKey siteKey ) throws VerticalUserServicesException, VerticalRemoveException, VerticalSecurityException, RemoteException { String message = "OperationWrapper REMOVE not implemented."; VerticalUserServicesLogger.error( message ); } protected void handlerCustom( HttpServletRequest request, HttpServletResponse response, HttpSession session, ExtendedMap formItems, UserServicesService userServices, SiteKey siteKey, String operation ) throws VerticalUserServicesException, VerticalEngineException, IOException, ClassNotFoundException, IllegalAccessException, InstantiationException { String message = "Custom operation not implemented: {0}"; if ( operation != null ) { operation = operation.toUpperCase(); } VerticalUserServicesLogger.error( message, operation, null ); } protected void handlerUpdate( HttpServletRequest request, HttpServletResponse response, HttpSession session, ExtendedMap formItems, UserServicesService userServices, SiteKey siteKey ) { String message = "OperationWrapper UPDATE not implemented."; VerticalUserServicesLogger.error( message ); } private UserServicesService lookupUserServices() { return userServicesService; } public boolean isArrayFormItem( Map formItems, String string ) { if ( !formItems.containsKey( string ) ) { return false; } return formItems.get( string ).getClass() == String[].class; } private ExtendedMap parseSimpleRequest( HttpServletRequest request ) { ExtendedMap formItems = new ExtendedMap( true ); Enumeration paramNames = request.getParameterNames(); while ( paramNames.hasMoreElements() ) { String key = paramNames.nextElement().toString(); String[] values = request.getParameterValues( key ); if ( values != null ) { if ( values.length == 1 && values[0] != null ) { String value = values[0]; if ( "true".equals( value ) ) { formItems.putBoolean( key, true ); } else if ( "false".equals( value ) ) { formItems.putBoolean( key, false ); } else { formItems.putString( key, value ); } } else if ( values.length > 1 ) { formItems.put( key, values ); } } else { formItems.put( key, "" ); } } return formItems; } private ExtendedMap parseMultiPartRequest( HttpServletRequest request ) { ExtendedMap formItems = new ExtendedMap( true ); try { List paramList = fileUpload.parseRequest( new ServletRequestContext( request ) ); for ( Iterator iter = paramList.iterator(); iter.hasNext(); ) { FileItem fileItem = (FileItem) iter.next(); String name = fileItem.getFieldName(); if ( fileItem.isFormField() ) { String value = fileItem.getString( "UTF-8" ); if ( formItems.containsKey( name ) ) { ArrayList<Object> values = new ArrayList<Object>(); Object obj = formItems.get( name ); if ( obj instanceof Object[] ) { String[] objArray = (String[]) obj; for ( int i = 0; i < objArray.length; i++ ) { values.add( objArray[i] ); } } else { values.add( obj ); } values.add( value ); formItems.put( name, values.toArray( new String[values.size()] ) ); } else { formItems.put( name, value ); } } else { if ( fileItem.getSize() > 0 ) { if ( formItems.containsKey( name ) ) { ArrayList<Object> values = new ArrayList<Object>(); Object obj = formItems.get( name ); if ( obj instanceof FileItem[] ) { FileItem[] objArray = (FileItem[]) obj; for ( int i = 0; i < objArray.length; i++ ) { values.add( objArray[i] ); } } else { values.add( obj ); } values.add( fileItem ); formItems.put( name, values.toArray( new FileItem[values.size()] ) ); } else { formItems.put( name, fileItem ); } } } } } catch ( FileUploadException fue ) { String message = "Error occurred with file upload: %t"; VerticalAdminLogger.error( message, fue ); } catch ( UnsupportedEncodingException uee ) { String message = "Character encoding not supported: %t"; VerticalAdminLogger.error( message, uee ); } // Add parameters from url Map paramMap = request.getParameterMap(); for ( Iterator iter = paramMap.entrySet().iterator(); iter.hasNext(); ) { Map.Entry entry = (Map.Entry) iter.next(); String key = (String) entry.getKey(); if ( entry.getValue() instanceof String[] ) { String[] values = (String[]) entry.getValue(); for ( int i = 0; i < values.length; i++ ) { formItems.put( key, values[i] ); } } else { formItems.put( key, entry.getValue() ); } } return formItems; } private ExtendedMap parseForm( HttpServletRequest request ) { if ( FileUploadBase.isMultipartContent( new ServletRequestContext( request ) ) ) { return parseMultiPartRequest( request ); } else { return parseSimpleRequest( request ); } } /** * Process incoming HTTP requests. */ private ModelAndView handleRequestInternal( HttpServletRequest request, HttpServletResponse response, SitePath sitePath ) throws IOException { HttpSession session = request.getSession( true ); ExtendedMap formItems = parseForm( request ); UserServicesService userServices = lookupUserServices(); SiteKey siteKey = sitePath.getSiteKey(); SitePath originalSitePath = (SitePath) request.getAttribute( Attribute.ORIGINAL_SITEPATH ); String handler = UserServicesParameterResolver.resolveHandlerFromSitePath( originalSitePath ); String operation = UserServicesParameterResolver.resolveOperationFromSitePath( originalSitePath ); if ( !userServicesAccessManager.isOperationAllowed( siteKey, handler, operation ) ) { String message = "Access to http service '" + handler + "." + operation + "' on site " + siteKey + " is not allowed by configuration. Check the settings in site-" + siteKey + ".properties"; VerticalUserServicesLogger.warn( message ); String httpErrorMsg = "Access denied to http service '" + handler + "." + operation + "' on site " + siteKey; response.sendError( HttpServletResponse.SC_FORBIDDEN, httpErrorMsg ); return null; } // check if domain in redirect URL is allowed final String redirect = formItems.getString( "_redirect", null ); final String redirectUrl = userServicesRedirectUrlResolver.resolveRedirectUrlToPage( request, redirect, null ); if ( !isRedirectUrlAllowed( redirectUrl ) ) { final String domain = new URL( redirectUrl ).getHost(); final String message = String.format( "Domain '%s' of redirect URL not allowed (%s), in request to HTTP service '%s.%s' on site %s. " + "Check setting 'cms.httpServices.redirect.allowedDomains' in cms.properties", domain, redirectUrl, handler, operation, siteKey ); VerticalUserServicesLogger.warn( message ); final String httpErrorMsg = String.format( "Domain of redirect URL not allowed: %s", domain ); response.sendError( HttpServletResponse.SC_FORBIDDEN, httpErrorMsg ); return null; } try { if ( !( this instanceof FormServicesProcessor ) ) { // Note: The FormHandlerController is doing its own validation. Boolean captchaOk = captchaService.validateCaptcha( formItems, request, handler, operation ); if ( ( captchaOk != null ) && ( !captchaOk ) ) { VerticalSession vsession = (VerticalSession) session.getAttribute( VerticalSession.VERTICAL_SESSION_OBJECT ); if ( vsession == null ) { vsession = new VerticalSession(); session.setAttribute( VerticalSession.VERTICAL_SESSION_OBJECT, vsession ); } vsession.setAttribute( "error_" + handler + "_" + operation, captchaService.buildErrorXMLForSessionContext( formItems ).getAsDOMDocument() ); redirectToErrorPage( request, response, ERR_INVALID_CAPTCHA ); return null; } } if ( "create".equals( operation ) ) { handlerCreate( request, response, session, formItems, userServices, siteKey ); } else if ( "update".equals( operation ) ) { handlerUpdate( request, response, session, formItems, userServices, siteKey ); } else if ( "remove".equals( operation ) ) { handlerRemove( request, response, session, formItems, userServices, siteKey ); } else { handlerCustom( request, response, session, formItems, userServices, siteKey, operation ); } } catch ( VerticalSecurityException vse ) { // If user = anonymous, 401 (Unauthorized), otherwise (403) forbidden. String message = "No rights to handle request: %t"; VerticalUserServicesLogger.warn( message, vse ); redirectToErrorPage( request, response, ERR_SECURITY_EXCEPTION ); } catch ( ContentAccessException vse ) { // If user = anonymous, 401 (Unauthorized), otherwise (403) forbidden. String message = "No rights to handle request: %t"; VerticalUserServicesLogger.warn( message, vse ); redirectToErrorPage( request, response, ERR_SECURITY_EXCEPTION ); } catch ( CategoryAccessException vse ) { // If user = anonymous, 401 (Unauthorized), otherwise (403) forbidden. String message = "No rights to handle request: %t"; VerticalUserServicesLogger.warn( message, vse ); redirectToErrorPage( request, response, ERR_SECURITY_EXCEPTION ); } catch ( HttpServicesException hse ) { throw hse; } catch ( Exception e ) { // 500, Internal Server Error String message = "Failed to handle request: %t"; VerticalUserServicesLogger.error( message, e ); redirectToErrorPage( request, response, ERR_OPERATION_BACKEND ); } return null; } protected void redirectToPage( HttpServletRequest request, HttpServletResponse response, ExtendedMap formItems ) { redirectToPage( request, response, formItems, null ); } protected void redirectToPage( HttpServletRequest request, HttpServletResponse response, ExtendedMap formItems, MultiValueMap queryParams ) { String redirect = formItems.getString( "_redirect", null ); String url = userServicesRedirectUrlResolver.resolveRedirectUrlToPage( request, redirect, queryParams ); if ( isAbsoluteUrl( url ) ) { siteRedirectHelper.sendRedirectWithAbsoluteURL( response, url ); } else { String decodedUrl = UrlPathDecoder.decode( url ); siteRedirectHelper.sendRedirectWithPath( request, response, decodedUrl ); } } private boolean isRedirectUrlAllowed( final String redirectUrl ) { return !isAbsoluteUrl( redirectUrl ) || isRedirectDomainAllowed( redirectUrl ); } private boolean isRedirectDomainAllowed( final String redirectUrl ) { try { final URL url = new URL( redirectUrl ); final String domain = url.getHost().toLowerCase(); for ( String allowedDomain : this.allowedRedirectDomains ) { if ( "*".equals( allowedDomain ) || domain.equals( allowedDomain ) || domain.endsWith( "." + allowedDomain ) ) { return true; } } return false; } catch ( MalformedURLException e ) { return false; } } private boolean isAbsoluteUrl( String url ) { return url.matches( "^[a-z]{3,6}://.+" ); } protected void redirectToErrorPage( HttpServletRequest request, HttpServletResponse response, int code ) { Integer[] codes = new Integer[1]; codes[0] = code; redirectToErrorPage( request, response, codes, this ); } protected void redirectToErrorPage( HttpServletRequest request, HttpServletResponse response, Integer[] codes, ServicesProcessor errorSource ) { String url = userServicesRedirectUrlResolver.resolveRedirectUrlToErrorPage( request, codes, errorSource ); siteRedirectHelper.sendRedirect( request, response, url ); } protected static String createMissingParametersMessage( String operation, List<String> missingParameters ) { StringBuffer message = new StringBuffer(); message.append( operation ).append( " : Missing " ).append( missingParameters.size() ).append( " parameters: " ); boolean isFirst = true; for ( String missingParameter : missingParameters ) { if ( isFirst ) { isFirst = false; } else { message.append( ", " ); } message.append( missingParameter ); } return message.toString(); } protected static List<String> findMissingRequiredParameters( String[] requiredParameters, ExtendedMap formItems, boolean allowEmpty ) { List<String> missingParameters = new ArrayList<String>(); for ( String requiredParameter : requiredParameters ) { if ( !formItems.containsKey( requiredParameter ) ) { missingParameters.add( requiredParameter ); continue; } String submittedValue = formItems.getString( requiredParameter ); if ( StringUtils.isEmpty( submittedValue ) && !allowEmpty ) { missingParameters.add( requiredParameter ); } } return missingParameters; } @Autowired public void setUserServicesAccessManager( UserServicesAccessManager userServicesAccessManager ) { this.userServicesAccessManager = userServicesAccessManager; } @Value("${cms.name.transliterate}") public void setTransliterate( boolean transliterate ) { this.transliterate = transliterate; } @Value("${cms.httpServices.redirect.allowedDomains}") public void setAllowedRedirectDomains( final String allowedRedirectDomains ) { final Iterable<String> domainPrefixes = Splitter.on( "," ).omitEmptyStrings().trimResults().split( allowedRedirectDomains ); if ( Iterables.isEmpty( domainPrefixes ) ) { this.allowedRedirectDomains = ImmutableList.of( "*" ); } else { final ImmutableList.Builder<String> domainPrefixList = ImmutableList.builder(); for ( String domainPrefix : domainPrefixes ) { domainPrefixList.add( domainPrefix.toLowerCase() ); } this.allowedRedirectDomains = domainPrefixList.build(); } } public ModelAndView handleRequest( HttpServletRequest request, HttpServletResponse response ) throws Exception { // Get check and eventually set original sitePath SitePath originalSitePath = (SitePath) request.getAttribute( Attribute.ORIGINAL_SITEPATH ); if ( originalSitePath == null ) { originalSitePath = sitePathResolver.resolveSitePath( request ); request.setAttribute( Attribute.ORIGINAL_SITEPATH, originalSitePath ); } // Get and set the current sitePath SitePath currentSitePath = sitePathResolver.resolveSitePath( request ); return handleRequestInternal( request, response, currentSitePath ); } protected SiteContext getSiteContext( SiteKey siteKey ) { return siteService.getSiteContext( siteKey ); } protected SitePath getSitePath( HttpServletRequest request ) { SitePath sitePath = (SitePath) request.getAttribute( Attribute.ORIGINAL_SITEPATH ); if ( sitePath == null ) { sitePath = sitePathResolver.resolveSitePath( request ); } return sitePath; } @Override public Integer httpResponseCodeTranslator( final Integer[] errorCodes ) { if ( errorCodes.length != 1 ) { throw new HttpServicesException( ERR_OPERATION_BACKEND ); } Integer errorCode = errorCodes[0]; switch ( errorCode ) { case ERR_PARAMETERS_MISSING: case ERR_PARAMETERS_INVALID: case ERR_INVALID_CAPTCHA: case ERR_CONTENT_NOT_FOUND: return HTTP_STATUS_BAD_REQUEST; case ERR_SECURITY_EXCEPTION: if ( securityService.getLoggedInPortalUserAsEntity().isAnonymous() ) { return HTTP_STATUS_UNAUTHORIZED; } else { return HTTP_STATUS_FORBIDDEN; } case ERR_OPERATION_BACKEND: case ERR_EMAIL_SEND_FAILED: return HTTP_STATUS_INTERNAL_SERVER_ERROR; default: throw new HttpServicesException( ERR_OPERATION_BACKEND ); } } }