<?php
/**
  * This file contains the HelloCaptcha class that is the essential
  * PHP class for embedding HelloCaptcha CAPTCHAs.
  * 
  * The most recent version of this file can be downloaded from:
  * http://www.hellocaptcha.com/dl/HelloCaptcha.class.php
  *
  * A sample file that demonstrates how to use this class can be
  * downloaded from:
  * http://www.hellocaptcha.com/dl/sample-captcha.php
  *
  * PHP versions before 5.2 do not have JSON support enabled. To solve
  * this issue download JSON.php from our server and place it near the
  * HelloCaptcha.class.php file. The HelloCaptcha.class.php file
  * automatically includes it if neccessary. The file can be downloaded
  * from:
  * http://www.hellocaptcha.com/dl/JSON.php
  *
  * @see http://www.hellocaptcha.com
  * @see http://www.hellocaptcha.com/doc/embedding
  * @Copyright Creo Group, 2010-2012 (www.creo.hu)
  */
  
/** Version */
define("HELLOCAPTCHA_VERSION", "1.1");
/** Release date */
define("HELLOCAPTCHA_RELEASE_DATE", "2011-10-11");

/**
  * This id will be used, if profile is not set. You can override this
  * constant by defining it before including this file.
  */
define("HELLOCAPTCHA_TRIAL_PROFILE_ID", "hellocaptchatrialprofile");
/** HelloCaptcha service domain */
define("HELLOCAPTCHA_SERVER_DOMAIN", "www.hellocaptcha.com");
/** Path for verifying user's answer */
define("HELLOCAPTCHA_GET_NEW_TEST_PATH", "/service/getnewtest/");
/** Path for verifying user's answer */
define("HELLOCAPTCHA_VERIFY_ANSWER_PATH", "/service/verifyanswer/");

/** Error codes for getNewTest */
define("HELLOCAPTCHA_ERROR_1", "The profile named profileid does not exist.");
define("HELLOCAPTCHA_ERROR_2", "There is no pre-rendered CAPTCHA in the profile pool, yet. Please wait, until HelloCaptcha server generates the CAPTCHA animations.");

/** Error codes for verifyAnswer */
define("HELLOCAPTCHA_ERROR_3", "The Turing test identified by turingtestid does not exist.");
define("HELLOCAPTCHA_ERROR_4", "Either test turingtestid or answer is empty.");
define("HELLOCAPTCHA_ERROR_5", "The Turing test was already verified once.");
define("HELLOCAPTCHA_ERROR_6", "The Turing test is expired.");
define("HELLOCAPTCHA_ERROR_7", "There are too many bad tries (on a retryable CAPTCHA).");

/**
  * Class that wraps functionality of HelloCaptcha service by implementing HTTP based
  * communication and creating wrapper methods for HelloCaptcha service. This class
  * should be used if someone needs custom functionality of this service. The HelloCaptcha
  * class is based on the functions of this class.
  *
  * @version 1.0
  */
class HelloCaptchaWrapper {

  /**
    * Converts an error code to string
    *
    * @param int $p_error_code
    * @return string response
    */
  public static function errorCodeToString( $p_errorcode ) {
    $cn = "HELLOCAPTCHA_ERROR_" . $p_errorcode;
    if ( defined( $cn ) )
      return constant( $cn );
    else
      return "Unknown error.";
  }

  /**
    * Submits an HTTP POST to a server and returns its result.
    *
    * @param string $p_host
    * @param string $p_path
    * @param array $p_data
    * @return string response
    */
  private static function doHTTPPost( $p_host, $p_path, $p_data ) {

    $port = (isset($_SERVER['HTTPS'])) ? 443 : 80;
    $sslhost = (isset($_SERVER['HTTPS'])) ? $sslhost = "ssl://" . $p_host : $p_host;

    // Url encode the passed values
    $request = Array();
    foreach ( $p_data as $key => $value ) {
      $request[] = $key . '=' . urlencode( stripslashes($value) );
    }
    $request = implode('&', $request);

    // Create HTTP request string
    $http_request  = "POST $p_path HTTP/1.0\r\n";
    $http_request .= "Host: $p_host\r\n";
    $http_request .= "Content-Type: application/x-www-form-urlencoded;\r\n";
    $http_request .= "Content-Length: " . strlen($request) . "\r\n";
    $http_request .= "User-Agent: HELLOCAPTCHA/PHP\r\n";
    $http_request .= "\r\n";
    $http_request .= $request;

    // Connect to server
    if( false == ( $fs = @fsockopen($sslhost, $port, $errno, $errstr, 10) ) )
      throw new Exception( __METHOD__ . ': Cannot connect to server: ' . $sslhost . ' on port: ' . $port . '.' );

    // Do the request
    fwrite($fs, $http_request);

    // Get the response
    $response = '';
    while ( !feof($fs) ) {
      // One TCP-IP packet
      $response .= fgets($fs, 1160);
    }

    // Disconnect from the server
    fclose($fs);

    // Return the response
    return $response;
  }

  /**
    * Decodes a server response response is valid. If the response contains an exception
    * then throws it.
    */
  protected static function decodeResponse( $p_response ) {
    $p_response = explode("\r\n\r\n", $p_response, 2);
    $p_response = $p_response[1];

    // Check response for trivial errors
    if ( is_null( $p_response ) )
      throw new Exception( __METHOD__ . ': Server response is NULL.' );
    if ( empty( $p_response ) )
      throw new Exception( __METHOD__ . ': Server response is EMPTY.' );

    // JSON decode
    $response = json_decode( $p_response, TRUE );
    if ( NULL === $response )
      throw new Exception( __METHOD__ . ': Error decoding server response.' );

    // Check for exception
    if ( !empty( $response['e'] ) )
      throw new Exception( __METHOD__ . ': Server returned an error: ' . $response['e']['message'], $response['e']['code'] );

    // Return value if not empty
    if ( empty( $response['d'] ) )
      throw new Exception( __METHOD__ . ': Server returned empty data' );
    return $response['d'];
  }

  /**
    * Calls remote service and return the result.
    *
    * @param string $p_path
    * @param array $p_data
    * @return array response
    */
  protected static function callRemote( $p_path, $p_data ) {

    // Post data over HTTP
    $response = self::doHTTPPost( HELLOCAPTCHA_SERVER_DOMAIN, $p_path, $p_data );

    // Decode and return response
    $response = self::decodeResponse( $response );
    return $response;
  }

  /**
    * Creates a new Turing test and returns the received result.
    *
    * @param string $p_profileid
    * @return array response
    */
  public function getNewTest( $p_profileid = HELLOCAPTCHA_TRIAL_PROFILE_ID ) {

    // Call remote service
    return self::callRemote(
      HELLOCAPTCHA_GET_NEW_TEST_PATH,
      Array( 'profileid' => $p_profileid )
    );
  }

  /**
    * Checks the answer for a given Turing test.
    *
    * @param string $p_turingtestid
    * @param string $p_answer
    * @return array response
    */
  public function verifyAnswer( $p_turingtestid, $p_answer ) {

    // Assert Turing test id is not empty
    if ( empty( $p_turingtestid ) )
      die (__METHOD__ . ": Turing test id is empty. Please specify Turing test id to let the system know which test was completed. A new test id can be obtained on http://www.hellocaptcha.com/registration/ .");
    // Assert answer is not empty
    if ( empty( $p_answer ) )
      die (__METHOD__ . ": Answer is empty. Please specify answer to let the system know what to evaluate.");

    // Call remote service
    return self::callRemote(
      HELLOCAPTCHA_VERIFY_ANSWER_PATH,
      Array( 'turingtestid' => $p_turingtestid, 'answer' => $p_answer )
    );
  }

}

/**
  * Class that provides high level functionality for HelloCaptcha service.
  * This class uses the low level methods of HelloCaptchaWrapper class. If you need
  * custom behaviour, you can create a custom class (MyHelloCaptcha) by modifying this
  * class or by derivating from it.
  */
class HelloCaptcha extends HelloCaptchaWrapper {

  /**
    * Asks for a new test on HelloCaptcha server and returns a default
    * embedding code for the CAPTCHA of the given profile.
    */
  public static function getEmbedCode( $p_profileid = HELLOCAPTCHA_TRIAL_PROFILE_ID  ) {
    try {
      // Call wrapper method, and return the ready-made part of the answer.
      $res = self::getNewTest( $p_profileid );
      return $res['html'];
    } catch (Exception $e) {
      echo "Error acquiring HelloCaptcha embedding code. " . self::errorCodeToString( $e->getCode() );
      error_log( __METHOD__ . ": Unexpected error occured. Error message: " . self::errorCodeToString( $e->getCode() ) ); 
      error_log( __METHOD__ . ": Unexpected error occured. HelloCaptcha server said: " . $e->getMessage() ); 
    }
  }

  public static function checkAnswer( $p_turingtestid, $p_answer ) {
    try {
      // Call wrapper method, and return the validity of the answer.
      $res = self::verifyAnswer( $p_turingtestid, $p_answer );
      return $res['is_valid'];
    } catch (Exception $e) {
      echo "Error checking user answer for HelloCaptcha. " . self::errorCodeToString( $e->getCode() ) . "<br />";
      error_log( __METHOD__ . ": Unexpected error occured. Error message: " . self::errorCodeToString( $e->getCode() ) );
      error_log( __METHOD__ . ": Unexpected error occured. HelloCaptcha server said: " . $e->getMessage() );
      return false;
    }
  }

}

if ( !function_exists('json_decode') ) {
  function json_decode($p_content, $p_assoc = true) {
    require_once 'JSON.php';
    if ( $p_assoc ) {
      $json = new Services_JSON(SERVICES_JSON_LOOSE_TYPE);
    } else {
      $json = new Services_JSON;
    }
    return $json->decode($p_content);
  }
}

if ( !function_exists('json_encode') ){
  function json_encode($p_content){
    require_once 'JSON.php';
    $json = new Services_JSON;
    return $json->encode($p_content);
  }
}

?>
