/*
 *  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.scan;

import java.awt.Dimension;
import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import java.io.BufferedReader;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.util.ArrayList;
import java.util.List;
import java.util.Stack;

import javax.swing.JComponent;
import javax.swing.JLabel;
import javax.swing.JPanel;

import com.eviware.soapui.SoapUI;
import com.eviware.soapui.config.CrossSiteScriptingScanConfig;
import com.eviware.soapui.config.SecurityScanConfig;
import com.eviware.soapui.config.StrategyTypeConfig;
import com.eviware.soapui.impl.wsdl.teststeps.RestRequestStepResult;
import com.eviware.soapui.impl.wsdl.teststeps.RestTestRequestStep;
import com.eviware.soapui.impl.wsdl.teststeps.WsdlTestRequestStepResult;
import com.eviware.soapui.impl.wsdl.teststeps.WsdlTestStep;
import com.eviware.soapui.model.ModelItem;
import com.eviware.soapui.model.security.SecurityCheckedParameter;
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.assertion.CrossSiteScriptAssertion;
import com.eviware.soapui.support.SecurityScanUtil;
import com.eviware.soapui.support.UISupport;
import com.eviware.soapui.support.types.StringToStringMap;
import com.eviware.soapui.support.xml.XmlObjectTreeModel;
import com.eviware.soapui.support.xml.XmlObjectTreeModel.XmlTreeNode;
import com.eviware.x.form.support.ADialogBuilder;
import com.eviware.x.form.support.AField;
import com.eviware.x.form.support.AForm;
import com.eviware.x.form.support.AField.AFieldType;
import com.eviware.x.impl.swing.JFormDialog;
import com.eviware.x.impl.swing.JStringListFormField;

/**
 * This checks whether any parameters sent in the request are included in the
 * response, If they do appear, this is a good parameter to look at as a
 * possible attack vector for XSS
 * 
 * @author nebojsa.tasic
 */

public class CrossSiteScriptingScan extends AbstractSecurityScanWithProperties
{

	public static final String TYPE = "CrossSiteScriptingScan";
	public static final String NAME = "Cross Site Scripting";
	public static final String PARAMETER_EXPOSURE_SCAN_CONFIG = "CrossSiteScriptingScanConfig";
	public static final String TEST_CASE_RUNNER = "testCaseRunner";
	public static final String TEST_STEP = "testStep";
	private CrossSiteScriptingScanConfig cssConfig;
	StrategyTypeConfig.Enum strategy = StrategyTypeConfig.ONE_BY_ONE;

	List<String> defaultParameterExposureStrings = new ArrayList<String>();
	private JFormDialog dialog;

	public CrossSiteScriptingScan( TestStep testStep, SecurityScanConfig config, ModelItem parent, String icon )
	{
		super( testStep, config, parent, icon );
		if( config.getConfig() == null || !( config.getConfig() instanceof CrossSiteScriptingScanConfig ) )
			initConfig();
		else
			cssConfig = ( CrossSiteScriptingScanConfig )getConfig().getConfig();

	}

	private void initDefaultVectors()
	{
		try
		{
			InputStream in = SoapUI.class.getResourceAsStream( "/com/eviware/soapui/resources/security/XSS-vectors.txt" );
			BufferedReader br = new BufferedReader( new InputStreamReader( in ) );
			String strLine;
			while( ( strLine = br.readLine() ) != null )
			{
				defaultParameterExposureStrings.add( strLine );
			}
			in.close();
		}
		catch( Exception e )
		{
			SoapUI.logError( e );
		}

	}

	@Override
	protected void initAssertions()
	{
		super.initAssertions();

		if( assertionsSupport.getAssertionByName( CrossSiteScriptAssertion.LABEL ) == null )
		{
			assertionsSupport.addWsdlAssertion( CrossSiteScriptAssertion.LABEL );
		}
	}

	private void initConfig()
	{
		initDefaultVectors();
		getConfig().setConfig( CrossSiteScriptingScanConfig.Factory.newInstance() );
		cssConfig = ( CrossSiteScriptingScanConfig )getConfig().getConfig();

		cssConfig.setParameterExposureStringsArray( defaultParameterExposureStrings
				.toArray( new String[defaultParameterExposureStrings.size()] ) );
	}

	@Override
	public void updateSecurityConfig( SecurityScanConfig config )
	{
		super.updateSecurityConfig( config );

		if( cssConfig != null )
		{
			cssConfig = ( CrossSiteScriptingScanConfig )getConfig().getConfig();
		}
	}

	@Override
	protected void execute( SecurityTestRunner securityTestRunner, TestStep testStep, SecurityTestRunContext context )
	{
		sendToContext( context, testStep, securityTestRunner );
		PropertyMutation mutation = PropertyMutation.popMutation( context );
		if( mutation != null )
		{
			if( testStep instanceof RestTestRequestStep )
			{
				RestRequestStepResult message = ( RestRequestStepResult )mutation.getTestStep().run(
						( TestCaseRunner )securityTestRunner, context );
				message.setRequestContent( "" );
				createMessageExchange(  mutation.getMutatedParameters(), message, context );
			}
			else
			{
				WsdlTestRequestStepResult message = ( WsdlTestRequestStepResult )mutation.getTestStep().run(
						( TestCaseRunner )securityTestRunner, context );
				message.setRequestContent( "", false );
				createMessageExchange( mutation.getMutatedParameters(), message, context );
			}
		}
	}

	private void sendToContext( SecurityTestRunContext context, TestStep testStep, SecurityTestRunner securityTestRunner )
	{
		context.put( TEST_CASE_RUNNER, securityTestRunner );
		context.put( TEST_STEP, testStep );
	}

	private void removeFromContext( SecurityTestRunContext context )
	{
		context.remove( TEST_CASE_RUNNER );
		context.remove( TEST_STEP );
	}

	@Override
	public JComponent getComponent()
	{
		JPanel p = UISupport.createEmptyPanel( 5, 75, 0, 5 );
		p.add( new JLabel( "Strings for Cross Site Scripting can be configured under Advanced Settings" ) );
		return p;
	}

	@Override
	public String getType()
	{
		return TYPE;
	}

	@SuppressWarnings( "unchecked" )
	@Override
	protected boolean hasNext( TestStep testStep, SecurityTestRunContext context )
	{
		if( !context.hasProperty( PropertyMutation.REQUEST_MUTATIONS_STACK ) )
		{
			Stack<PropertyMutation> requestMutationsList = new Stack<PropertyMutation>();
			context.put( PropertyMutation.REQUEST_MUTATIONS_STACK, requestMutationsList );
			context.put( PARAMETER_EXPOSURE_SCAN_CONFIG, cssConfig );
			try
			{
				extractMutations( testStep, context );
			}
			catch( Exception e )
			{
				SoapUI.logError( e );
			}

			return checkIfEmptyStack( context );
		}

		Stack<PropertyMutation> stack = ( Stack<PropertyMutation> )context.get( PropertyMutation.REQUEST_MUTATIONS_STACK );
		if( stack.empty() )
		{
			context.remove( PropertyMutation.REQUEST_MUTATIONS_STACK );
			context.remove( PARAMETER_EXPOSURE_SCAN_CONFIG );
			removeFromContext( context );
			return false;
		}
		else
		{
			return true;
		}
	}

	@SuppressWarnings( "unchecked" )
	private boolean checkIfEmptyStack( SecurityTestRunContext context )
	{
		Stack<PropertyMutation> stack = ( Stack<PropertyMutation> )context.get( PropertyMutation.REQUEST_MUTATIONS_STACK );
		if( stack.empty() )
			return false;
		else
			return true;
	}

	private void extractMutations( TestStep testStep, SecurityTestRunContext context )
	{
		strategy = getExecutionStrategy().getStrategy();
		for( String value : cssConfig.getParameterExposureStringsList() )
		{
			// property expansion support
			value = context.expand( value );

			PropertyMutation allAtOncePropertyMutation = new PropertyMutation();
			TestStep testStepCopy = null;
			XmlObjectTreeModel model = null;
			List<SecurityCheckedParameter> scpList = getParameterHolder().getParameterList();
			StringToStringMap stsmap = new StringToStringMap();
			for( SecurityCheckedParameter scp : scpList )
			{

				if( strategy.equals( StrategyTypeConfig.ONE_BY_ONE ) )
				{
					stsmap = new StringToStringMap();
					model = SecurityScanUtil.getXmlObjectTreeModel( testStep, scp );
					testStepCopy = SecurityTestRunnerImpl.cloneTestStepForSecurityScan( ( WsdlTestStep )testStep );
				}
				else
				{
					if( model == null )
					{
						model = SecurityScanUtil.getXmlObjectTreeModel( testStep, scp );
					}
					if( testStepCopy == null )
					{
						testStepCopy = SecurityTestRunnerImpl.cloneTestStepForSecurityScan( ( WsdlTestStep )testStep );
					}
				}

				// if parameter is xml
				if( scp.isChecked() && scp.getXpath().trim().length() > 0 )
				{
					XmlTreeNode[] treeNodes = null;

					treeNodes = model.selectTreeNodes( context.expand( scp.getXpath() ) );

					if( treeNodes.length > 0 )
					{
						XmlTreeNode mynode = treeNodes[0];

						// work only for simple types
						if( mynode.isLeaf() )
						{
							mynode.setValue( 1, value );

							if( strategy.equals( StrategyTypeConfig.ONE_BY_ONE ) )
							{
								PropertyMutation oneByOnePropertyMutation = new PropertyMutation();
								oneByOnePropertyMutation.setPropertyName( scp.getName() );
								oneByOnePropertyMutation.setPropertyValue( unescapEscaped( model.getXmlObject().toString() ) );
								stsmap.put( scp.getLabel(), mynode.getNodeText() );
								oneByOnePropertyMutation.setMutatedParameters( stsmap );
								oneByOnePropertyMutation.updateRequestProperty( testStepCopy );
								oneByOnePropertyMutation.setTestStep( testStepCopy );
								oneByOnePropertyMutation.addMutation( context );
							}
							else
							{
								allAtOncePropertyMutation.setPropertyName( scp.getName() );
								allAtOncePropertyMutation.setPropertyValue( unescapEscaped( model.getXmlObject().toString() ) );
								stsmap.put( scp.getLabel(), mynode.getNodeText() );
								allAtOncePropertyMutation.setMutatedParameters( stsmap );
								allAtOncePropertyMutation.updateRequestProperty( testStepCopy );
								allAtOncePropertyMutation.setTestStep( testStepCopy );

							}
						}
					}
				}
				// non xml parameter
				else
				{
					if( strategy.equals( StrategyTypeConfig.ONE_BY_ONE ) )
					{
						PropertyMutation oneByOnePropertyMutation = new PropertyMutation();
						oneByOnePropertyMutation.setPropertyName( scp.getName() );
						oneByOnePropertyMutation.setPropertyValue( value );
						stsmap.put( scp.getLabel(), value );
						oneByOnePropertyMutation.setMutatedParameters( stsmap );
						oneByOnePropertyMutation.updateRequestProperty( testStepCopy );
						oneByOnePropertyMutation.setTestStep( testStepCopy );
						oneByOnePropertyMutation.addMutation( context );
					}
					else
					{
						allAtOncePropertyMutation.setPropertyName( scp.getName() );
						allAtOncePropertyMutation.setPropertyValue( value );
						stsmap.put( scp.getLabel(), value );
						allAtOncePropertyMutation.setMutatedParameters( stsmap );
						allAtOncePropertyMutation.updateRequestProperty( testStepCopy );
						allAtOncePropertyMutation.setTestStep( testStepCopy );
					}
				}
			}

			if( strategy.equals( StrategyTypeConfig.ALL_AT_ONCE ) )
			{
				allAtOncePropertyMutation.addMutation( context );
			}
		}
	}

	private String unescapEscaped( String value )
	{
		return value.replaceAll( "&lt;", "<" );
	}

	@Override
	public String getConfigDescription()
	{
		return "Configures parameter exposure security scan";
	}

	@Override
	public String getConfigName()
	{
		return "Cross Site Scripting Scan";
	}

	@Override
	public String getHelpURL()
	{
		return "http://soapui.org/Security/cross-site-scripting.html";
	}

	@Override
	public JComponent getAdvancedSettingsPanel()
	{
		dialog = ( JFormDialog )ADialogBuilder.buildDialog( AdvancedSettings.class );
		JStringListFormField stringField = ( JStringListFormField )dialog
				.getFormField( AdvancedSettings.PARAMETER_EXPOSURE_STRINGS );
		stringField.setOptions( cssConfig.getParameterExposureStringsList().toArray() );
		stringField.setProperty( "dimension", new Dimension( 470, 150 ) );
		stringField.getComponent().addPropertyChangeListener( "options", new PropertyChangeListener()
		{

			@Override
			public void propertyChange( PropertyChangeEvent evt )
			{
				String[] newOptions = ( String[] )evt.getNewValue();
				String[] oldOptions = ( String[] )evt.getOldValue();
				// added
				if( newOptions.length > oldOptions.length )
				{
					// new element is always added to the end
					String[] newValue = ( String[] )evt.getNewValue();
					String itemToAdd = newValue[newValue.length - 1];
					cssConfig.addParameterExposureStrings( itemToAdd );
				}
				// removed
				if( newOptions.length < oldOptions.length )
				{
					/*
					 * items with same index should me same. first one in oldOptions
					 * that does not match is element that is removed.
					 */
					for( int cnt = 0; cnt < oldOptions.length; cnt++ )
					{
						if( cnt < newOptions.length )
						{
							if( newOptions[cnt] != oldOptions[cnt] )
							{
								cssConfig.removeParameterExposureStrings( cnt );
								break;
							}
						}
						else
						{
							// this is border case, last lement in array is removed.
							cssConfig.removeParameterExposureStrings( oldOptions.length - 1 );
						}
					}
				}
			}
		} );

		return dialog.getPanel();
	}

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

		super.release();
	}

	@AForm( description = "Cross Site Scripting", name = "Cross Site Scripting" )
	protected interface AdvancedSettings
	{

		@AField( description = "Cross Site Scripting Vectors", name = "###Cross Site Scripting", type = AFieldType.STRINGLIST )
		public final static String PARAMETER_EXPOSURE_STRINGS = "###Cross Site Scripting";

	}
}
