<commit_before><?php
/**
 * @file
 * Provides webhook endpoint for GoCardless.
 *
 * @todo this should probably be implemented as an IPN and the code moved into
 * CRM_Core_Payment_GoCardless::handlePaymentNotification() However, I was not
 * sure about how CiviCRM processes those as it seems to rely on $_REQUEST
 * which does strange things with JSON input.
 */

require_once 'CRM/Core/Page.php';

class CRM_GoCardless_Page_Webhook extends CRM_Core_Page {

  /**
   * @var array names (string) to values (int) */
  public $contribution_status_map;
  public static $implemented_webhooks = [
    'payments' => ['confirmed', 'failed'],
    'subscriptions'  => ['cancelled', 'finished'],
  ];
  /**
   * @var bool */
  protected $test_mode;

  /**
   * @var array of webhook events that we can process */
  public $events;
  /**
   * @var CRM_Core_Payment_GoCardless payment processor loaded from CiviCRM API paymentProcessor entity */
  protected $payment_processor;
  /**
   * Timestamp for logs.
   * @var string
   */
  public $now;

  public function run() {

    // We need to check the input against the test and live payment processors.
    $raw_payload = file_get_contents('php://input');
    if (!function_exists('getallheaders')) {
      // https://github.com/artfulrobot/uk.artfulrobot.civicrm.gocardless/issues/23
      // Some server configs do not provide getallheaders().
      // We only care about the Webhook-Signature header so try to extract that from $_SERVER.
      $headers = [];
      if (isset($_SERVER['HTTP_WEBHOOK_SIGNATURE'])) {
        $headers['Webhook-Signature'] = $_SERVER['HTTP_WEBHOOK_SIGNATURE'];
      }
    }
    else {
      $headers = getallheaders();
    }

    // debugging:
    $this->now = date('Y-m-d:H:i:s');
    CRM_Core_Error::debug_log_message("Webhook $this->now body:\n" . $raw_payload, FALSE, 'GoCardless', PEAR_LOG_INFO);
    CRM_Core_Error::debug_log_message("Webhook $this->now headers:\n" . json_encode($headers), FALSE, 'GoCardless', PEAR_LOG_INFO);

    try {
      $this->parseWebhookRequest($headers, $raw_payload);
    }
    catch (InvalidArgumentException $e) {
      // Invalid webhook call.
      header("HTTP/1.1 498 Invalid Token");
      CRM_Utils_System::civiExit();
    }

    // Process the events
    header("HTTP/1.1 204 OK");
    $this->processWebhookEvents();
    CRM_Utils_System::civiExit();
  }

  /**
   * Loop the events and process them.
   *
   * @param bool $throw whether to silently log exceptions or chuck them up for
   * someone else to notice. Useful for phpunit tests.
   */
  public function processWebhookEvents($throw = FALSE) {
    foreach ($this->events as $event) {
      try {
        $method = 'do' . ucfirst($event->resource_type) . ucfirst($event->action);
        $this->$method($event);
      }
      catch (CRM_GoCardless_WebhookEventIgnoredException $e) {
        CRM_Core_Error::debug_log_message("Webhook $this->now Ignored webhook event. Reason: " . $e->getMessage(), FALSE, 'GoCardless', PEAR_LOG_NOTICE);
        if ($throw) {
          throw $e;
        }
      }
      catch (Exception $e) {
        CRM_Core_Error::debug_log_message("Webhook $this->now Failed event. Reason: " . $e->getMessage(), FALSE, 'GoCardless', PEAR_LOG_ERR);
        if ($throw) {
          throw $e;
        }
      }
    }
  }

  /**
   * Check incomming input for validity and extract the data into properties.
   *
   * Alters $this->test_mode, $this->events.
   *
   * @throws InvalidArgumentException if signature does not match.
   * @return void
   */
  public function parseWebhookRequest($headers, $raw_payload) {

    // Check signature and find appropriate Payment Processor.
    if (empty($headers["Webhook-Signature"])) {
      throw new InvalidArgumentException("Unsigned API request.");
    }
    $provided_signature = $headers["Webhook-Signature"];

    // Loop through all GoCardless Payment Processors until we find one for which the signature is valid.
    $candidates = civicrm_api3('PaymentProcessor', 'get', ['payment_processor_type_id' => "GoCardless", 'is_active' => 1]);
    $valid = FALSE;
    foreach ($candidates['values'] as $payment_processor_id => $pp) {
      $token = isset($pp['signature']) ? $pp['signature'] : '';
      $calculated_signature = hash_hmac("sha256", $raw_payload, $token);
      if ($token && $provided_signature === $calculated_signature) {
        $valid = TRUE;
        $this->test_mode = !empty($pp['is_test']);
        $this->payment_processor = Civi\Payment\System::singleton()->getByProcessor($pp);
        break;
      }
    }
    if (!$valid) {
      throw new InvalidArgumentException("Invalid signature in request. (Or payment processor is not active)");
    }
    $data = json_decode($raw_payload);
    if (!$data || empty($data->events)) {
      throw new InvalidArgumentException("Invalid or missing data in request.");
    }<commit_msg>Treat webhook headers as case-sensitive for webhooks - GC change<commit_after><?php
/**
 * @file
 * Provides webhook endpoint for GoCardless.
 *
 * @todo this should probably be implemented as an IPN and the code moved into
 * CRM_Core_Payment_GoCardless::handlePaymentNotification() However, I was not
 * sure about how CiviCRM processes those as it seems to rely on $_REQUEST
 * which does strange things with JSON input.
 */

require_once 'CRM/Core/Page.php';

class CRM_GoCardless_Page_Webhook extends CRM_Core_Page {

  /**
   * @var array names (string) to values (int) */
  public $contribution_status_map;
  public static $implemented_webhooks = [
    'payments' => ['confirmed', 'failed'],
    'subscriptions'  => ['cancelled', 'finished'],
  ];
  /**
   * @var bool */
  protected $test_mode;

  /**
   * @var array of webhook events that we can process */
  public $events;
  /**
   * @var CRM_Core_Payment_GoCardless payment processor loaded from CiviCRM API paymentProcessor entity */
  protected $payment_processor;
  /**
   * Timestamp for logs.
   * @var string
   */
  public $now;

  public function run() {

    // We need to check the input against the test and live payment processors.
    $raw_payload = file_get_contents('php://input');
    if (!function_exists('getallheaders')) {
      // https://github.com/artfulrobot/uk.artfulrobot.civicrm.gocardless/issues/23
      // Some server configs do not provide getallheaders().
      // We only care about the Webhook-Signature header so try to extract that from $_SERVER.
      $headers = [];
      if (isset($_SERVER['HTTP_WEBHOOK_SIGNATURE'])) {
        $headers['Webhook-Signature'] = $_SERVER['HTTP_WEBHOOK_SIGNATURE'];
      }
    }
    else {
      $headers = getallheaders();
    }

    // debugging:
    $this->now = date('Y-m-d:H:i:s');
    CRM_Core_Error::debug_log_message("Webhook $this->now body:\n" . $raw_payload, FALSE, 'GoCardless', PEAR_LOG_INFO);
    CRM_Core_Error::debug_log_message("Webhook $this->now headers:\n" . json_encode($headers), FALSE, 'GoCardless', PEAR_LOG_INFO);

    try {
      $this->parseWebhookRequest($headers, $raw_payload);
    }
    catch (InvalidArgumentException $e) {
      // Invalid webhook call.
      header("HTTP/1.1 498 Invalid Token");
      CRM_Utils_System::civiExit();
    }

    // Process the events
    header("HTTP/1.1 204 OK");
    $this->processWebhookEvents();
    CRM_Utils_System::civiExit();
  }

  /**
   * Loop the events and process them.
   *
   * @param bool $throw whether to silently log exceptions or chuck them up for
   * someone else to notice. Useful for phpunit tests.
   */
  public function processWebhookEvents($throw = FALSE) {
    foreach ($this->events as $event) {
      try {
        $method = 'do' . ucfirst($event->resource_type) . ucfirst($event->action);
        $this->$method($event);
      }
      catch (CRM_GoCardless_WebhookEventIgnoredException $e) {
        CRM_Core_Error::debug_log_message("Webhook $this->now Ignored webhook event. Reason: " . $e->getMessage(), FALSE, 'GoCardless', PEAR_LOG_NOTICE);
        if ($throw) {
          throw $e;
        }
      }
      catch (Exception $e) {
        CRM_Core_Error::debug_log_message("Webhook $this->now Failed event. Reason: " . $e->getMessage(), FALSE, 'GoCardless', PEAR_LOG_ERR);
        if ($throw) {
          throw $e;
        }
      }
    }
  }

  /**
   * Check incomming input for validity and extract the data into properties.
   *
   * Alters $this->test_mode, $this->events.
   *
   * @throws InvalidArgumentException if signature does not match.
   *
   * @param array $headers
   * @param string $raw_payload
   *
   * @return void
   */
  public function parseWebhookRequest($headers, $raw_payload) {

    // Check signature and find appropriate Payment Processor.
    // GoCardless announced in Jan 2020 that their headers would now be sent
    // lowercase and must be treated as case-insensitive.
    $provided_signature = NULL;
    foreach ($headers as $key => $value) {
      if (strtolower($key) === 'webhook-signature') {
        $provided_signature = $value;
        break;
      }
    }
    if (empty($provided_signature)) {
      throw new InvalidArgumentException("Unsigned API request.");
    }

    // Loop through all GoCardless Payment Processors until we find one for which the signature is valid.
    $candidates = civicrm_api3('PaymentProcessor', 'get', ['payment_processor_type_id' => "GoCardless", 'is_active' => 1]);
    $valid = FALSE;
    foreach ($candidates['values'] as $payment_processor_id => $pp) {
      $token = isset($pp['signature']) ? $pp['signature'] : '';
      $calculated_signature = hash_hmac("sha256", $raw_payload, $token);
      if ($token && $provided_signature === $calculated_signature) {
        $valid = TRUE;
        $this->test_mode = !empty($pp['is_test']);
        $this->payment_processor = Civi\Payment\System::singleton()->getByProcessor($pp);
        break;
      }
    }
    if (!$valid) {
      throw new InvalidArgumentException("Invalid signature in request. (Or payment processor is not active)");
    }
    $data = json_decode($raw_payload);
    if (!$data || empty($data->events)) {
      throw new InvalidArgumentException("Invalid or missing data in request.");
    }
{'fraction_non_alphanumeric': 0.13070866141732285, 'fraction_numerical': 0.0037401574803149606, 'mean_word_length': 3.2091963545981774, 'pattern_counts': {'":': 0, '<': 5, '<?xml version=': 0, '>': 52, 'https://': 2, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '497101', 'n_tokens_mistral': 3137, 'n_tokens_neox': 3044, 'n_words': 814}