/*
 *  soapUI, copyright (C) 2004-2012 smartbear.com 
 *
 *  soapUI is free software; you can redistribute it and/or modify it under the 
 *  terms of version 2.1 of the GNU Lesser General Public License as published by 
 *  the Free Software Foundation.
 *
 *  soapUI 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 at gnu.org.
 */
package com.eviware.soapui.security.assertion;

import java.awt.Dimension;
import java.awt.event.ActionEvent;
import java.util.ArrayList;
import java.util.List;

import javax.swing.AbstractAction;
import javax.swing.Action;
import javax.swing.JScrollPane;

import org.apache.xmlbeans.XmlObject;

import com.eviware.soapui.SoapUI;
import com.eviware.soapui.config.CrossSiteScriptingScanConfig;
import com.eviware.soapui.config.HttpRequestConfig;
import com.eviware.soapui.config.TestAssertionConfig;
import com.eviware.soapui.config.TestStepConfig;
import com.eviware.soapui.impl.support.HttpUtils;
import com.eviware.soapui.impl.wsdl.panels.assertions.AssertionListEntry;
import com.eviware.soapui.impl.wsdl.panels.teststeps.support.AbstractGroovyEditorModel;
import com.eviware.soapui.impl.wsdl.support.HelpUrls;
import com.eviware.soapui.impl.wsdl.testcase.WsdlTestCase;
import com.eviware.soapui.impl.wsdl.teststeps.HttpTestRequestStep;
import com.eviware.soapui.impl.wsdl.teststeps.WsdlMessageAssertion;
import com.eviware.soapui.impl.wsdl.teststeps.WsdlTestStep;
import com.eviware.soapui.impl.wsdl.teststeps.assertions.AbstractTestAssertionFactory;
import com.eviware.soapui.impl.wsdl.teststeps.registry.HttpRequestStepFactory;
import com.eviware.soapui.impl.wsdl.teststeps.registry.WsdlTestStepFactory;
import com.eviware.soapui.impl.wsdl.teststeps.registry.WsdlTestStepRegistry;
import com.eviware.soapui.model.ModelItem;
import com.eviware.soapui.model.TestPropertyHolder;
import com.eviware.soapui.model.iface.MessageExchange;
import com.eviware.soapui.model.iface.SubmitContext;
import com.eviware.soapui.model.testsuite.Assertable;
import com.eviware.soapui.model.testsuite.AssertionError;
import com.eviware.soapui.model.testsuite.AssertionException;
import com.eviware.soapui.model.testsuite.ResponseAssertion;
import com.eviware.soapui.model.testsuite.TestCaseRunner;
import com.eviware.soapui.model.testsuite.TestStep;
import com.eviware.soapui.security.SecurityTestRunContext;
import com.eviware.soapui.security.SecurityTestRunner;
import com.eviware.soapui.security.SecurityTestRunnerImpl;
import com.eviware.soapui.security.scan.CrossSiteScriptingScan;
import com.eviware.soapui.support.SecurityScanUtil;
import com.eviware.soapui.support.UISupport;
import com.eviware.soapui.support.components.GroovyEditorComponent;
import com.eviware.soapui.support.scripting.SoapUIScriptEngine;
import com.eviware.soapui.support.scripting.SoapUIScriptEngineRegistry;
import com.eviware.soapui.support.xml.XmlObjectConfigurationBuilder;
import com.eviware.soapui.support.xml.XmlObjectConfigurationReader;
import com.eviware.x.form.XFormDialog;
import com.eviware.x.form.XFormField;
import com.eviware.x.form.XFormFieldListener;
import com.eviware.x.form.support.ADialogBuilder;
import com.eviware.x.form.support.AField;
import com.eviware.x.form.support.AField.AFieldType;
import com.eviware.x.form.support.AForm;

public class CrossSiteScriptAssertion extends WsdlMessageAssertion implements ResponseAssertion
{
	public static final String ID = "CrosSiteScript";
	public static final String LABEL = "Cross Site Scripting Detection";
	public static final String DESCRIPTION = "Cross Site Scripting....assertion for...";
	public static final String GROOVY_SCRIPT = "groovyScript";
	public static final String CHECK_RESPONSE = "checkResponse";
	public static final String CHECK_SEPARATE_HTML = "checkSeparateHTML";

	private XFormDialog dialog;
	private String script;
	private GroovyEditorModel groovyEditorModel;
	private SoapUIScriptEngine scriptEngine;

	MessageExchange messageExchange;
	SubmitContext context;

	private boolean checkResponse;
	private boolean checkSeparateHTML;

	public CrossSiteScriptAssertion( TestAssertionConfig assertionConfig, Assertable assertable )
	{
		super( assertionConfig, assertable, false, true, false, true );
		groovyEditorModel = new GroovyEditorModel( this );
		init();
		scriptEngine = SoapUIScriptEngineRegistry.create( this );
	}

	private void init()
	{
		XmlObjectConfigurationReader reader = new XmlObjectConfigurationReader( getConfiguration() );
		script = reader.readString( GROOVY_SCRIPT, "" );
		checkResponse = reader.readBoolean( CHECK_RESPONSE, true );
		checkSeparateHTML = reader.readBoolean( CHECK_SEPARATE_HTML, false );
		groovyEditorModel.setScript( script );
	}

	@Override
	protected String internalAssertResponse( MessageExchange messageExchange, SubmitContext context )
			throws AssertionException
	{
		TestStep testStep = ( TestStep )context.getProperty( CrossSiteScriptingScan.TEST_STEP );
		testStep = SecurityTestRunnerImpl.cloneTestStepForSecurityScan( ( WsdlTestStep )testStep );
		SecurityTestRunner securityTestRunner = ( SecurityTestRunner )context
				.getProperty( CrossSiteScriptingScan.TEST_CASE_RUNNER );

		List<String> urls = submitScript( messageExchange, context );

		CrossSiteScriptingScanConfig parameterExposureCheckConfig = ( CrossSiteScriptingScanConfig )context
				.getProperty( CrossSiteScriptingScan.PARAMETER_EXPOSURE_SCAN_CONFIG );

		List<AssertionError> assertionErrorList = new ArrayList<AssertionError>();
		boolean throwExceptionCheckResponse = false;

		if( checkResponse )
		{
			throwExceptionCheckResponse = checkResponse( messageExchange, context, parameterExposureCheckConfig,
					assertionErrorList );
		}

		boolean throwExceptionCheckSeparateHTML = false;
		if( checkSeparateHTML )
		{
			throwExceptionCheckSeparateHTML = checkSeparateHTML( messageExchange, context, testStep, securityTestRunner,
					urls, parameterExposureCheckConfig, assertionErrorList );
		}

		if( throwExceptionCheckResponse || throwExceptionCheckSeparateHTML )
		{
			throw new AssertionException( assertionErrorList.toArray( new AssertionError[assertionErrorList.size()] ) );
		}

		return "OK";
	}

	private boolean checkSeparateHTML( MessageExchange messageExchange, SubmitContext context, TestStep testStep,
			SecurityTestRunner securityTestRunner, List<String> urls,
			CrossSiteScriptingScanConfig parameterExposureCheckConfig, List<AssertionError> assertionErrorList )
	{
		boolean throwException = false;
		for( String url : urls )
		{
			HttpTestRequestStep httpRequest = createHttpRequest( ( WsdlTestStep )testStep, url );
			MessageExchange messageExchange2 = ( MessageExchange )httpRequest.run( ( TestCaseRunner )securityTestRunner,
					( SecurityTestRunContext )context );

			for( String value : parameterExposureCheckConfig.getParameterExposureStringsList() )
			{
				value = context.expand( value );// property expansion support
				String match = SecurityScanUtil.contains( context, new String( messageExchange2.getRawResponseData() ),
						value, false );
				if( match != null )
				{
					String shortValue = value.length() > 25 ? value.substring( 0, 22 ) + "... " : value;
					String message = "XSS content sent in request '" + shortValue + "' is exposed in response on link "
							+ url + " . Possibility for XSS script attack in: " + messageExchange.getModelItem().getName();
					assertionErrorList.add( new AssertionError( message ) );
					throwException = true;
				}
			}
		}
		return throwException;
	}

	private boolean checkResponse( MessageExchange messageExchange, SubmitContext context,
			CrossSiteScriptingScanConfig parameterExposureCheckConfig, List<AssertionError> assertionErrorList )
	{
		boolean throwException = false;
		for( String value : parameterExposureCheckConfig.getParameterExposureStringsList() )
		{
			value = context.expand( value );// property expansion support
			String match = SecurityScanUtil.contains( context, new String( messageExchange.getRawResponseData() ), value,
					false );
			if( match != null )
			{
				String shortValue = value.length() > 25 ? value.substring( 0, 22 ) + "... " : value;
				String message = "Content that is sent in request '" + shortValue
						+ "' is exposed in response. Possibility for XSS script attack in: "
						+ messageExchange.getModelItem().getName();
				assertionErrorList.add( new AssertionError( message ) );
				throwException = true;
			}
		}
		return throwException;
	}

	private List<String> submitScript( MessageExchange messageExchange, SubmitContext context )
	{
		List<String> urls = new ArrayList<String>();
		scriptEngine.setScript( script );
		scriptEngine.setVariable( "urls", urls );
		scriptEngine.setVariable( "messageExchange", messageExchange );
		this.messageExchange = messageExchange;
		scriptEngine.setVariable( "context", context );
		this.context = context;
		scriptEngine.setVariable( "log", SoapUI.ensureGroovyLog() );

		try
		{
			Object result = scriptEngine.run();
			if( result instanceof List )
			{
				urls = ( List<String> )result;
			}
		}
		catch( Exception ex )
		{
			SoapUI.logError( ex );
		}
		finally
		{
			scriptEngine.clearVariables();
		}
		return urls;
	}

	private HttpTestRequestStep createHttpRequest( WsdlTestStep testStep2, String url )
	{
		HttpRequestConfig httpRequest = HttpRequestConfig.Factory.newInstance();
		httpRequest.setEndpoint( HttpUtils.ensureEndpointStartsWithProtocol( url ) );
		httpRequest.setMethod( "GET" );

		TestStepConfig testStepConfig = TestStepConfig.Factory.newInstance();
		testStepConfig.setType( HttpRequestStepFactory.HTTPREQUEST_TYPE );
		testStepConfig.setConfig( httpRequest );
		testStepConfig.setName( "Separate Request" );

		WsdlTestStepFactory factory = WsdlTestStepRegistry.getInstance().getFactory(
				( HttpRequestStepFactory.HTTPREQUEST_TYPE ) );
		return ( HttpTestRequestStep )factory.buildTestStep( ( WsdlTestCase )testStep2.getTestCase(), testStepConfig,
				false );

	}

	protected String internalAssertProperty( TestPropertyHolder source, String propertyName,
			MessageExchange messageExchange, SubmitContext context ) throws AssertionException
	{
		//		return internalAssertResponse( messageExchange, context );
		return null;
	}

	public static class Factory extends AbstractTestAssertionFactory
	{
		public Factory()
		{
			super( CrossSiteScriptAssertion.ID, CrossSiteScriptAssertion.LABEL, CrossSiteScriptAssertion.class,
					CrossSiteScriptingScan.class );

		}

		@Override
		public String getCategory()
		{
			return "";
		}

		@Override
		public Class<? extends WsdlMessageAssertion> getAssertionClassType()
		{
			return CrossSiteScriptAssertion.class;
		}

		@Override
		public AssertionListEntry getAssertionListEntry()
		{
			return new AssertionListEntry( CrossSiteScriptAssertion.ID, CrossSiteScriptAssertion.LABEL,
					CrossSiteScriptAssertion.DESCRIPTION );
		}
	}

	@Override
	protected String internalAssertRequest( MessageExchange messageExchange, SubmitContext context )
			throws AssertionException
	{
		return null;
	}

	protected XmlObject createConfiguration()
	{
		XmlObjectConfigurationBuilder builder = new XmlObjectConfigurationBuilder();
		builder.add( GROOVY_SCRIPT, script );
		builder.add( CHECK_RESPONSE, checkResponse );
		builder.add( CHECK_SEPARATE_HTML, checkSeparateHTML );
		return builder.finish();
	}

	public boolean configure()
	{
		if( dialog == null )
			buildDialog();

		dialog.show();
		if( dialog.getReturnValue() == XFormDialog.OK_OPTION )
		{
			checkResponse = Boolean.valueOf( dialog.getFormField( CrossSiteScripSeparateHTMLConfigDialog.CHECK_RESPONSE )
					.getValue() );
			checkSeparateHTML = Boolean.valueOf( dialog.getFormField(
					CrossSiteScripSeparateHTMLConfigDialog.CHECK_SEPARATE_HTML ).getValue() );
			setConfiguration( createConfiguration() );
		}
		return true;
	}

	private class GroovyEditorModel extends AbstractGroovyEditorModel
	{
		@Override
		public Action createRunAction()
		{
			return new AbstractAction()
			{
				public void actionPerformed( ActionEvent e )
				{
					Object result = null;
					List<String> urls = new ArrayList<String>();
					scriptEngine.setScript( script );
					scriptEngine.setVariable( "urls", urls );
					scriptEngine.setVariable( "messageExchange", messageExchange );
					scriptEngine.setVariable( "context", context );
					scriptEngine.setVariable( "log", SoapUI.ensureGroovyLog() );

					try
					{
						result = scriptEngine.run();
						if( result instanceof List )
						{
							urls = ( List<String> )result;
						}
						String generatedUrls = "";
						for( String url : urls )
						{
							generatedUrls += "\n" + url;
						}
						UISupport.showInfoMessage( "Generated urls :" + generatedUrls + " \n\nScript result"
								+ ( ( result == null ) ? "" : ": " + result + "" ) );
					}
					catch( Exception ex )
					{
						SoapUI.logError( ex );
					}
					finally
					{
						scriptEngine.clearVariables();
					}
				}
			};
		}

		public GroovyEditorModel( ModelItem modelItem )
		{
			super( new String[] { "urls", "log", "context", "messageExchange" }, modelItem, "" );
		}

		public String getScript()
		{
			return script;
		}

		public void setScript( String text )
		{
			script = text;
		}
	}

	protected GroovyEditorComponent buildGroovyPanel()
	{
		return new GroovyEditorComponent( groovyEditorModel, null );
	}

	protected void buildDialog()
	{
		dialog = ADialogBuilder.buildDialog( CrossSiteScripSeparateHTMLConfigDialog.class );
		dialog.setSize( 600, 600 );
		dialog.setBooleanValue( CrossSiteScripSeparateHTMLConfigDialog.CHECK_RESPONSE, checkResponse );
		dialog.setBooleanValue( CrossSiteScripSeparateHTMLConfigDialog.CHECK_SEPARATE_HTML, checkSeparateHTML );
		final GroovyEditorComponent groovyEditorComponent = buildGroovyPanel();
		dialog.getFormField( CrossSiteScripSeparateHTMLConfigDialog.GROOVY ).setProperty( "component",
				new JScrollPane( groovyEditorComponent ) );
		dialog.getFormField( CrossSiteScripSeparateHTMLConfigDialog.GROOVY ).setProperty( "dimension",
				new Dimension( 450, 400 ) );
		dialog.getFormField( CrossSiteScripSeparateHTMLConfigDialog.CHECK_SEPARATE_HTML ).addFormFieldListener(
				new XFormFieldListener()
				{

					@Override
					public void valueChanged( XFormField sourceField, String newValue, String oldValue )
					{
						groovyEditorComponent.setEnabled( new Boolean( newValue ) );
					}

				} );

		groovyEditorComponent.setEnabled( checkSeparateHTML );
	}

	@Override
	public void release()
	{
		if( dialog != null )
			dialog.release();

		super.release();
	}

	@AForm( description = "", name = "Cross Site Scripting on Separate HTML", helpUrl = HelpUrls.SECURITY_XSS_ASSERTION_HELP )
	protected interface CrossSiteScripSeparateHTMLConfigDialog
	{
		@AField( description = "Check Imediate Response", name = "###Check Response", type = AFieldType.BOOLEAN )
		public final static String CHECK_RESPONSE = "###Check Response";

		@AField( description = "Check Response from URLs specified in Custom Script", name = "###Check Separate HTML", type = AFieldType.BOOLEAN )
		public final static String CHECK_SEPARATE_HTML = "###Check Separate HTML";

		@AField( description = "", name = "Enter Custom Script that returns a list of URLs to check for Cross Site Scripts ", type = AFieldType.LABEL )
		public final static String LABEL = "Enter Custom Script that returns a list of URLs to check for Cross Site Scripts ";

		@AField( description = "Groovy script", name = "###Groovy url list", type = AFieldType.COMPONENT )
		public final static String GROOVY = "###Groovy url list";
	}
}
