package hirondelle.web4j;

import hirondelle.web4j.security.SafeText;
import hirondelle.web4j.model.ModelCtorException;
import hirondelle.web4j.readconfig.ConfigReader;
import java.util.*;
import java.lang.reflect.*;
import java.util.logging.*;
import hirondelle.web4j.util.Util;
import hirondelle.web4j.model.ConvertParam;
import hirondelle.web4j.BuildImpl;

/**
 Scan Model Objects for Cross-Site Scripting vulnerabilities, and use of unsupported base objects.
  
 <P>Here, 'base object' refers to <tt>Integer</tt>, <tt>Date</tt>, 
 and so on -- the basic building blocks passed to a Model Object constructor.
 
 <P>Model Objects are defined as public concrete classes having a constructor that throws 
 {@link hirondelle.web4j.model.ModelCtorException}. 
 
 <P>The checks performed by this class are :
<ul>
 <li>any <tt>public</tt> <tt>getXXX</tt> methods that return a <tt>String</tt> are identified as
 Cross-Site Scripting vulnerabilities. Model Objects that return text in general should use {@link SafeText} instead 
 of {@link String}.
 <li>any constructors taking an argument whose class is not supported according to 
 {@link hirondelle.web4j.model.ConvertParam#isSupported(Class)} are identified as errors.
</ul> 
 
 <P>When one of these checks fails, the incident is logged as a warning.
*/ 
final class CheckModelObjects {
  
  /** Performs checks on all Model Objects.  */
  void performChecks(){
    fLogger.config("Performing checks on Model Objects for Cross-Site Scripting vulnerabilities and unsupported constructor arguments.");
    Set<Class> allClasses = ConfigReader.fetchConcreteClasses();
    for (Class implClass: allClasses){
      if ( isPublicModelObject(implClass) ){
        scanForMethodsReturningString(implClass);
        scanForUnsupportedCtorArgs(implClass);
      }
    }
    logResults();
  }
  
  // PRIVATE //
  private final ConvertParam fConvertParam = BuildImpl.forConvertParam();
  private static final Logger fLogger = Util.getLogger(CheckModelObjects.class);
  private int fCountXSS;
  private int fCountUnsupportedCtorArg;
  
  private boolean isPublicModelObject(Class aConcreteClass){
    boolean result = false;
    if ( isPublic(aConcreteClass) ) {
      Constructor[] ctors = aConcreteClass.getDeclaredConstructors();
      for (Constructor ctor : ctors){
        List<Class> exceptionClasses = Arrays.asList(ctor.getExceptionTypes());
        if (exceptionClasses.contains(ModelCtorException.class)) {
          result = true;
          break;
        }
      }
    }
    return result;
  }
  
  private boolean isPublic(Class aClass){
    return Modifier.isPublic(aClass.getModifiers());
  }
  
  private boolean isPublic(Method aMethod){
    return Modifier.isPublic(aMethod.getModifiers());
  }
  
  private void scanForMethodsReturningString(Class aModelObject){
    Method[] methods = aModelObject.getDeclaredMethods();
    for(Method method : methods){
      if( isVulnerableMethod(method) ) {
        fLogger.warning("Public Model Object named " + aModelObject.getName() + " has a public method named " + Util.quote(method.getName()) + " that returns a String. Should it return SafeText instead? Possible vulnerability to Cross-Site Scripting attack.");
        fCountXSS++;
      }
    }
  }
  
  private boolean isVulnerableMethod(Method aMethod){
    return isPublic(aMethod) && aMethod.getName().startsWith("get") && aMethod.getReturnType().equals(String.class);
  }
  
  private void scanForUnsupportedCtorArgs(Class aClass){
    Constructor[] ctors = aClass.getDeclaredConstructors();
    for (Constructor ctor : ctors){
      List<Class> argClasses = Arrays.asList(ctor.getParameterTypes());
      for(Class argClass: argClasses){
        if( ! fConvertParam.isSupported(argClass) ){
          fLogger.warning("Model Object: " + aClass + ". Constructor has argument not supported by ConvertParam: " + argClass);
          fCountUnsupportedCtorArg++;
        }
      }
    }
  }
  
  private void logResults(){
    if(fCountXSS == 0){
      fLogger.config("** SUCCESS *** : Scanned Model Objects for Cross-Site Scripting vulnerabilities. Found 0 incidents.");
    }
    else {
      fLogger.warning("Scanned Model Objects for Cross-Site Scripting vulnerabilities. Found " + fCountXSS + " incident(s).");
    }
    
    if( fCountUnsupportedCtorArg == 0 ) {
      fLogger.config("** SUCCESS *** : Scanned Model Objects for unsupported constructor arguments. Found 0 incidents.");
    }
    else {
      fLogger.warning("Scanned Model Objects for unsupported constructor arguments. Found " + fCountUnsupportedCtorArg + " incident(s).");
    }
  }
}
