/*
* Copyright 2000-2013 Enonic AS
* http://www.enonic.com/license
*/
package com.enonic.cms.web.portal.exception;
import java.io.IOException;
import java.net.SocketException;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import com.google.common.base.Throwables;
import com.enonic.cms.core.Attribute;
import com.enonic.cms.core.BadRequestErrorType;
import com.enonic.cms.core.InvalidKeyException;
import com.enonic.cms.core.NotFoundErrorType;
import com.enonic.cms.core.Path;
import com.enonic.cms.core.SiteURLResolver;
import com.enonic.cms.core.StacktraceLoggingUnrequired;
import com.enonic.cms.core.portal.AbstractBaseError;
import com.enonic.cms.core.portal.ClientError;
import com.enonic.cms.core.portal.ContentNameMismatchClientError;
import com.enonic.cms.core.portal.ContentNameMismatchException;
import com.enonic.cms.core.portal.ForbiddenErrorType;
import com.enonic.cms.core.portal.PathRequiresAuthenticationException;
import com.enonic.cms.core.portal.ResourceNotFoundException;
import com.enonic.cms.core.portal.ServerError;
import com.enonic.cms.core.portal.UnauthorizedErrorType;
import com.enonic.cms.core.structure.SiteEntity;
import com.enonic.cms.core.structure.SiteKey;
import com.enonic.cms.core.structure.SitePath;
import com.enonic.cms.core.structure.menuitem.MenuItemEntity;
import com.enonic.cms.core.structure.menuitem.MenuItemKey;
import com.enonic.cms.store.dao.MenuItemDao;
import com.enonic.cms.store.dao.SiteDao;
import com.enonic.cms.web.error.ErrorDetails;
import com.enonic.cms.web.error.ErrorPageRenderer;
import com.enonic.cms.web.portal.PortalWebContext;
import com.enonic.cms.web.portal.SiteRedirectAndForwardHelper;
import com.enonic.cms.web.portal.attachment.AttachmentRequestException;
import com.enonic.cms.web.portal.image.ImageRequestException;
import com.enonic.cms.web.portal.page.DefaultRequestException;
@SuppressWarnings("UnusedDeclaration")
@Component
public final class ExceptionHandlerImpl
implements ExceptionHandler
{
private static final Logger LOG = LoggerFactory.getLogger( ExceptionHandlerImpl.class );
private static final String ATTRIBUTE_ALREADY_PROCESSING_EXCEPTION = "ALREADY_PROCESSING_EXCEPTION";
private SiteRedirectAndForwardHelper siteRedirectAndForwardHelper;
private SiteURLResolver siteURLResolver;
private MenuItemDao menuItemDao;
@Autowired
protected ErrorPageRenderer errorPageRenderer;
private boolean logRequestInfoOnException;
@Autowired
private SiteDao siteDao;
@Autowired
public void setSiteRedirectAndForwardHelper( SiteRedirectAndForwardHelper value )
{
this.siteRedirectAndForwardHelper = value;
}
@Autowired
public void setSiteURLResolver( SiteURLResolver value )
{
this.siteURLResolver = value;
}
@Autowired
public void setMenuItemDao( MenuItemDao menuItemDao )
{
this.menuItemDao = menuItemDao;
}
private boolean isConnectionError( final Throwable error )
{
final Throwable rootCause = Throwables.getRootCause( error );
return rootCause instanceof SocketException;
}
@Override
public void handle( final PortalWebContext context, final Exception outerException )
throws ServletException, IOException
{
if ( context.processingExceptionCount() >= 2 )
{
throw new Error( outerException );
}
final HttpServletRequest request = context.getRequest();
final HttpServletResponse response = context.getResponse();
final boolean builtInErrorPageIsAlsoFailing = resolveAlreadyProcessingExceptionCount( request ) >= 2;
if ( builtInErrorPageIsAlsoFailing )
{
throw new Error( outerException );
}
final Throwable causingException;
if ( isExceptionAnyOfThose( outerException, new Class[]{DefaultRequestException.class, AttachmentRequestException.class,
ImageRequestException.class} ) )
{
// Have to unwrap these exceptions to get the causing exception
causingException = outerException.getCause();
}
else
{
causingException = outerException;
}
logException( outerException, causingException, request );
if ( isConnectionError( causingException ) )
{
return;
}
final AbstractBaseError error = getError( causingException );
response.setStatus( error.getStatusCode() );
if ( context.isAlreadyProcessingException() )
{
serveBuiltInExceptionPage( context, error );
return;
}
context.increaseProcessingExceptionCount();
handleExceptions( context, causingException, error );
}
private void handleExceptions( final PortalWebContext context, final Throwable exception, final AbstractBaseError error )
throws ServletException, IOException
{
if ( isExceptionAnyOfThose( exception, new Class[]{InvalidKeyException.class} ) &&
( (InvalidKeyException) exception ).forClass( SiteKey.class ) )
{
serveBuiltInExceptionPage( context, error );
return;
}
else if ( exception instanceof UnauthorizedErrorType )
{
serveLoginPage( context );
return;
}
try
{
if ( hasErrorPage( context.getSiteKey() ) )
{
serveErrorPage( context, error );
return;
}
}
catch ( Exception e )
{
LOG.error( "Failed to get error page: " + e.getMessage(), e );
}
serveBuiltInExceptionPage( context, error );
}
private void logException( final Throwable outerException, final Throwable causingException, final HttpServletRequest request )
{
if ( isExceptionAnyOfThose( causingException, new Class[]{ResourceNotFoundException.class} ) )
{
ResourceNotFoundException resourceNotFoundException = (ResourceNotFoundException) causingException;
boolean ignore = resourceNotFoundException.endsWithIgnoreCase( "favicon.ico" ) ||
resourceNotFoundException.endsWithIgnoreCase( "robots.txt" );
if ( ignore )
{
// skipping logging
return;
}
}
if ( isExceptionAnyOfThose( causingException, new Class[]{PathRequiresAuthenticationException.class} ) )
{
// skipping logging
return;
}
final boolean outerExceptionIsPortalRequestException = isExceptionAnyOfThose( outerException,
new Class[]{DefaultRequestException.class,
AttachmentRequestException.class,
ImageRequestException.class,
ResourceNotFoundException.class}
);
final boolean innerExceptionIsQuietException =
isExceptionAnyOfThose( causingException, new Class[]{StacktraceLoggingUnrequired.class} );
if ( outerExceptionIsPortalRequestException && innerExceptionIsQuietException )
{
LOG.info( outerException.getMessage() );
}
else if ( isExceptionAnyOfThose( causingException, new Class[]{ForbiddenErrorType.class, UnauthorizedErrorType.class} ) )
{
LOG.debug( causingException.getMessage() );
}
else
{
StringBuilder message = new StringBuilder();
message.append( causingException.getMessage() ).append( "\n" );
if ( this.logRequestInfoOnException )
{
message.append( buildRequestInfo( request ) );
}
LOG.error( message.toString(), causingException );
}
}
private String buildRequestInfo( final HttpServletRequest request )
{
final StringBuilder s = new StringBuilder();
s.append( "Request information:\n" );
s.append( " - cms.originalURL: " ).append( request.getAttribute( Attribute.ORIGINAL_URL ) ).append( "\n" );
s.append( " - cms.originalSitePath: " ).append( request.getAttribute( Attribute.ORIGINAL_SITEPATH ) ).append( "\n" );
s.append( " - http.queryString: " ).append( request.getQueryString() ).append( "\n" );
s.append( " - http.requestURI: " ).append( request.getRequestURI() ).append( "\n" );
s.append( " - http.remoteAddress: " ).append( request.getRemoteAddr() ).append( "\n" );
s.append( " - http.remoteHost: " ).append( request.getRemoteHost() ).append( "\n" );
s.append( " - http.characterEncoding: " ).append( request.getCharacterEncoding() ).append( "\n" );
s.append( " - http.header.User-Agent: " ).append( request.getHeader( "User-Agent" ) ).append( "\n" );
s.append( " - http.header.Referer: " ).append( request.getHeader( "Referer" ) ).append( "\n" );
return s.toString();
}
@SuppressWarnings({"ThrowableInstanceNeverThrown"})
private AbstractBaseError getError( final Throwable exception )
{
if ( exception instanceof BadRequestErrorType )
{
return new ClientError( HttpServletResponse.SC_BAD_REQUEST, exception.getMessage(), exception );
}
else if ( exception instanceof NotFoundErrorType )
{
if ( exception instanceof ContentNameMismatchException )
{
ContentNameMismatchException contentNameMismatchException = (ContentNameMismatchException) exception;
return new ContentNameMismatchClientError( HttpServletResponse.SC_NOT_FOUND, exception.getMessage(), exception,
contentNameMismatchException.getContentKey(),
contentNameMismatchException.getRequestedContentName() );
}
return new ClientError( HttpServletResponse.SC_NOT_FOUND, exception.getMessage(), exception );
}
else if ( exception instanceof ForbiddenErrorType )
{
return new ClientError( HttpServletResponse.SC_FORBIDDEN, exception.getMessage(), exception );
}
else if ( exception instanceof UnauthorizedErrorType )
{
return new ClientError( HttpServletResponse.SC_UNAUTHORIZED, exception.getMessage(), exception );
}
else
{
return new ServerError( HttpServletResponse.SC_INTERNAL_SERVER_ERROR, exception.getMessage(), exception );
}
}
private void serveErrorPage( final PortalWebContext context, final AbstractBaseError error )
throws Exception
{
final HttpServletRequest request = context.getRequest();
final HttpServletResponse response = context.getResponse();
final SitePath sitePath = context.getSitePath();
request.setAttribute( Attribute.ORIGINAL_SITEPATH, sitePath );
final MenuItemKey errorPageKey = getErrorPage( context.getSiteKey() );
final SitePath errorPagePath = new SitePath( sitePath.getSiteKey(), new Path( resolveMenuItemPath( errorPageKey ) ) );
final String statusCodeString = String.valueOf( error.getStatusCode() );
errorPagePath.addParam( "http_status_code", statusCodeString );
errorPagePath.addParam( "exception_message", error.getMessage() );
if ( error instanceof ContentNameMismatchClientError )
{
final ContentNameMismatchClientError contentNameMismatchClientError = (ContentNameMismatchClientError) error;
errorPagePath.addParam( "content_key", contentNameMismatchClientError.getContentKey().toString() );
}
siteRedirectAndForwardHelper.forward( request, response, errorPagePath );
}
private String resolveMenuItemPath( final MenuItemKey menuItemKey )
{
final MenuItemEntity menuItem = menuItemDao.findByKey( menuItemKey );
if ( menuItem == null )
{
return "";
}
return menuItem.getPathAsString();
}
private void serveBuiltInExceptionPage( final PortalWebContext context, final AbstractBaseError e )
throws IOException
{
this.errorPageRenderer.render( context.getResponse(), new ErrorDetails( context.getRequest(), e, e.getStatusCode() ) );
}
private void serveLoginPage( final PortalWebContext context )
throws ServletException, IOException
{
final HttpServletRequest request = context.getRequest();
final HttpServletResponse response = context.getResponse();
final SitePath unauthorizedPageSitePath = context.getSitePath();
final SiteKey siteKey = unauthorizedPageSitePath.getSiteKey();
final MenuItemKey menuItemKey = getLoginPage( siteKey );
if ( menuItemKey != null )
{
final Path loginPageLocalPath = new Path( resolveMenuItemPath( menuItemKey ) );
final SitePath loginPageSitePath =
unauthorizedPageSitePath.createNewInSameSite( loginPageLocalPath, unauthorizedPageSitePath.getParams() );
// remove the id param, because we may have got that from the unauthorizedPageSitePath
loginPageSitePath.removeParam( "id" );
// we dont want the error code in the referrer, so remove it
unauthorizedPageSitePath.removeParam( "error_user_login" );
final String referrer = siteURLResolver.createUrl( request, unauthorizedPageSitePath, true );
loginPageSitePath.addParam( "referrer", referrer );
siteRedirectAndForwardHelper.forward( request, response, loginPageSitePath );
}
else
{
response.setHeader( "WWW-Authenticate", "Basic" );
}
}
private int resolveAlreadyProcessingExceptionCount( final HttpServletRequest request )
{
final Integer alreadyProcessingExceptionCount = (Integer) request.getAttribute( ATTRIBUTE_ALREADY_PROCESSING_EXCEPTION );
if ( alreadyProcessingExceptionCount == null )
{
return 0;
}
else
{
return alreadyProcessingExceptionCount;
}
}
private void increaseAlreadyProcessingCounter( final HttpServletRequest request )
{
if ( request.getAttribute( ATTRIBUTE_ALREADY_PROCESSING_EXCEPTION ) != null )
{
Integer count = (Integer) request.getAttribute( ATTRIBUTE_ALREADY_PROCESSING_EXCEPTION );
request.setAttribute( ATTRIBUTE_ALREADY_PROCESSING_EXCEPTION, ++count );
}
else
{
request.setAttribute( ATTRIBUTE_ALREADY_PROCESSING_EXCEPTION, 1 );
}
}
private boolean isExceptionAnyOfThose( final Throwable e, final Class[] classes )
{
for ( Class cls : classes )
{
if ( cls.isInstance( e ) )
{
return true;
}
}
return false;
}
private MenuItemKey getErrorPage( final SiteKey siteKey )
{
final SiteEntity site = this.siteDao.findByKey( siteKey );
if ( site == null || ( site.getErrorPage() == null ) )
{
return null;
}
else
{
return site.getErrorPage().getKey();
}
}
private boolean hasErrorPage( final SiteKey siteKey )
{
return getErrorPage( siteKey ) != null;
}
private MenuItemKey getLoginPage( final SiteKey siteKey )
{
final SiteEntity site = this.siteDao.findByKey( siteKey );
if ( ( site == null ) || ( site.getLoginPage() == null ) )
{
return null;
}
else
{
return site.getLoginPage().getKey();
}
}
private boolean siteExists( final SiteKey siteKey )
{
final SiteEntity site = this.siteDao.findByKey( siteKey.toInt() );
return site != null;
}
@Value("${cms.render.logRequestInfoOnException}")
public void setLogRequestInfoOnException( final boolean value )
{
this.logRequestInfoOnException = value;
}
}