<?php

namespace WPLEClient;

use Exception;
use WPLEClient\Exceptions\LEFunctionsException;

/**
 * LetsEncrypt Functions class, supplying the LetsEncrypt Client with supportive functions.
 *
 * PHP version 5.2.0
 *
 * MIT License
 *
 * Copyright (c) 2018 Youri van Weegberg
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in all
 * copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
 * SOFTWARE.
 *
 * @author     Youri van Weegberg <youri@yourivw.nl>
 * @copyright  2018 Youri van Weegberg
 * @license    https://opensource.org/licenses/mit-license.php  MIT License
 * @link       https://github.com/yourivw/LEClient
 * @since      Class available since Release 1.0.0
 */
class LEFunctions
{
  /**
   * Generates a new RSA keypair and saves both keys to a new file.
   *
   * @param string	$directory		The directory in which to store the new keys. If set to null or empty string - privateKeyFile and publicKeyFile will be treated as absolute paths.
   * @param string	$privateKeyFile	The filename for the private key file.
   * @param string	$publicKeyFile  The filename for the public key file.
   * @param string	$keySize 		RSA key size, must be between 2048 and 4096 (default is 4096)
   */
  public static function RSAGenerateKeys($directory, $privateKeyFile = 'private.pem', $publicKeyFile = 'public.pem', $keySize = 4096)
  {
    if ($keySize < 2048 || $keySize > 4096) throw LEFunctionsException::InvalidArgumentException('RSA key size must be between 2048 and 4096.');

    $res = openssl_pkey_new(array(
      "private_key_type" => OPENSSL_KEYTYPE_RSA,
      "private_key_bits" => intval($keySize),
    ));

    if ($res === false) {
      $error = "Could not generate key pair! Check your OpenSSL configuration. OpenSSL Error: " . PHP_EOL;
      while ($message = openssl_error_string()) {
        $error .= $message . PHP_EOL;
      }
      throw LEFunctionsException::GenerateKeypairException($error);
    }

    if (!openssl_pkey_export($res, $privateKey)) {
      $error = "RSA keypair export failed!! Error: " . PHP_EOL;
      while ($message = openssl_error_string()) {
        $error .= $message . PHP_EOL;
      }
      throw LEFunctionsException::GenerateKeypairException($error);
    }

    $details = openssl_pkey_get_details($res);

    if ($directory !== null && $directory !== '') {
      $privateKeyFile = $directory . $privateKeyFile;
      $publicKeyFile = $directory . $publicKeyFile;
    }

    file_put_contents($privateKeyFile, $privateKey);
    file_put_contents($publicKeyFile, $details['key']);

    openssl_pkey_free($res);
  }

  /**
   * Generates a new EC prime256v1 keypair and saves both keys to a new file.
   *
   * @param string	$directory		The directory in which to store the new keys. If set to null or empty string - privateKeyFile and publicKeyFile will be treated as absolute paths.
   * @param string	$privateKeyFile	The filename for the private key file.
   * @param string	$publicKeyFile  The filename for the public key file.
   * @param string	$keysize  		EC key size, possible values are 256 (prime256v1) or 384 (secp384r1), default is 256
   */
  public static function ECGenerateKeys($directory, $privateKeyFile = 'private.pem', $publicKeyFile = 'public.pem', $keySize = 256)
  {
    if (version_compare(PHP_VERSION, '7.1.0') == -1) throw LEFunctionsException::PHPVersionException();

    if ($keySize == 256) {
      $res = openssl_pkey_new(array(
        "private_key_type" => OPENSSL_KEYTYPE_EC,
        "curve_name" => "prime256v1",
      ));
    } elseif ($keySize == 384) {
      $res = openssl_pkey_new(array(
        "private_key_type" => OPENSSL_KEYTYPE_EC,
        "curve_name" => "secp384r1",
      ));
    } else throw LEFunctionsException::InvalidArgumentException('EC key size must be 256 or 384.');


    if (!openssl_pkey_export($res, $privateKey)) throw LEFunctionsException::GenerateKeypairException('EC keypair export failed!');

    $details = openssl_pkey_get_details($res);

    if ($directory !== null && $directory !== '') {
      $privateKeyFile = $directory . $privateKeyFile;
      $publicKeyFile = $directory . $publicKeyFile;
    }

    file_put_contents($privateKeyFile, $privateKey);
    file_put_contents($publicKeyFile, $details['key']);

    openssl_pkey_free($res);
  }



  /**
   * Encodes a string input to a base64 encoded string which is URL safe.
   *
   * @param string	$input 	The input string to encode.
   *
   * @return string	Returns a URL safe base64 encoded string.
   */
  public static function Base64UrlSafeEncode($input)
  {
    return str_replace('=', '', strtr(base64_encode($input), '+/', '-_'));
  }

  /**
   * Decodes a string that is URL safe base64 encoded.
   *
   * @param string	$input	The encoded input string to decode.
   *
   * @return string	Returns the decoded input string.
   */
  public static function Base64UrlSafeDecode($input)
  {
    $remainder = strlen($input) % 4;
    if ($remainder) {
      $padlen = 4 - $remainder;
      $input .= str_repeat('=', $padlen);
    }
    return base64_decode(strtr($input, '-_', '+/'));
  }



  /**
   * Outputs a log message.
   *
   * @param object	$data		The data to print.
   * @param string	$function	The function name to print above. Defaults to the calling function's name from the stacktrace. (optional)
   */
  public static function log($data, $function = '')
  {
    $handle = fopen(WPLE_DEBUGGER . 'debug.log', 'a');
    $log = "[" . date('d-m-Y H:i:s') . "] :\n";
    $log .= $data . "\n\n";
    fwrite($handle, $log);
    fclose($handle);
  }



  /**
   * Makes a request to the HTTP challenge URL and checks whether the authorization is valid for the given $domain.
   *
   * @param string	$domain 			The domain to check the authorization for.
   * @param string 	$token 				The token (filename) to request.
   * @param string 	$keyAuthorization 	the keyAuthorization (file content) to compare.
   *
   * @return boolean	Returns true if the challenge is valid, false if not.
   */
  public static function checkHTTPChallenge($domain, $token, $keyAuthorization, $returncode = false)
  {
    if (!stripos($domain, 'http')) {
      $domain = 'http://' . $domain;
    } else {
      $domain = str_ireplace('https:', 'http:', $domain);
    }

    $requestURL = $domain . '/.well-known/acme-challenge/' . $token;

    if (!function_exists('curl_init')) {
      return false;
    }

    $curl = curl_init();

    curl_setopt_array($curl, array(
      CURLOPT_URL => $requestURL,
      CURLOPT_RETURNTRANSFER => true,
      CURLOPT_MAXREDIRS => 5,
      CURLOPT_TIMEOUT => 90,
      CURLOPT_FOLLOWLOCATION => true,
      CURLOPT_SSL_VERIFYHOST => false,
      CURLOPT_SSL_VERIFYPEER => false,
      CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
      CURLOPT_CUSTOMREQUEST => "GET",
      //CURLOPT_USERAGENT => 'Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/51.0.2704.103 Safari/537.36'
    ));

    $response = trim(curl_exec($curl));
    //$final_url = curl_getinfo($curl, CURLINFO_EFFECTIVE_URL);
    $httpcode = curl_getinfo($curl, CURLINFO_HTTP_CODE);

    curl_close($curl);

    if (!empty($response) && $response == $keyAuthorization) {
      return true;
    } else {

      //if(FALSE !== stripos($final_url, 'public_html')){
      //LEFunctions::log("Could not verify challenge code in above http challenge file. Trying again with advanced technique!.\n");
      //}

      if ($returncode) {
        return $httpcode;
      }

      return false;
    }
  }

  /**
   * Checks whether the applicable DNS TXT record is a valid authorization for the given $domain.
   *
   * @param string	$domain 	The domain to check the authorization for.
   * @param string	$DNSDigest	The digest to compare the DNS record to.
   *
   * @return boolean	Returns true if the challenge is valid, false if not.
   */
  public static function checkDNSChallenge($domain, $DNSDigest)
  {
    $requestURL = 'https://dns.google.com/resolve?name=_acme-challenge.' . $domain . '&type=TXT';
    $handle = curl_init();
    curl_setopt($handle, CURLOPT_URL, $requestURL);
    curl_setopt($handle, CURLOPT_RETURNTRANSFER, true);
    curl_setopt($handle, CURLOPT_FOLLOWLOCATION, true);
    curl_setopt($handle, CURLOPT_SSL_VERIFYPEER, false);
    $response = json_decode(trim(curl_exec($handle)));
    if ($response->Status === 0 && isset($response->Answer)) {
      foreach ($response->Answer as $answer) {
        if ($answer->type === 16) {
          if (str_ireplace('"', '', $answer->data) === $DNSDigest || $answer->data === $DNSDigest) return true;
        }
      }
    }
    return false;
  }

  /**
   * Creates a simple .htaccess file in $directory which denies from all.
   *
   * @param string	$directory	The directory in which to put the .htaccess file.
   */
  public static function createhtaccess($directory)
  {
    $htaccess = '<ifModule mod_authz_core.c>' . "\n"
      . '    Require all denied' . "\n"
      . '    <FilesMatch ^>' . "\n"
      . '    Require all denied' . "\n"
      . '    </FilesMatch>' . "\n"
      . '</ifModule>' . "\n"
      . '<ifModule !mod_authz_core.c>' . "\n"
      . '    Deny from all' . "\n"
      . '    <FilesMatch ^>' . "\n"
      . '    Deny from all' . "\n"
      . '    </FilesMatch>' . "\n"
      . '</ifModule>' . "\n";
    file_put_contents($directory . '.htaccess', $htaccess);
  }
}
