Commit 113517f0 authored by mattwire's avatar mattwire Committed by mattwire

Autocreate webhooks

parent 9bb2e557
......@@ -12,7 +12,7 @@ class CRM_Core_Payment_Stripe extends CRM_Core_Payment {
*
* @var string
*/
protected $_stripeAPIVersion = '2019-02-19';
const API_VERSION = '2019-02-19';
/**
* Mode of operation: live or test.
......@@ -21,6 +21,9 @@ class CRM_Core_Payment_Stripe extends CRM_Core_Payment {
*/
protected $_mode = NULL;
public static function getApiVersion() {
return self::API_VERSION;
}
/**
* Constructor
*
......@@ -35,6 +38,64 @@ class CRM_Core_Payment_Stripe extends CRM_Core_Payment {
$this->_processorName = ts('Stripe');
}
/**
* @param array $paymentProcessor
*
* @return string
*/
public static function getSecretKey($paymentProcessor) {
return trim(CRM_Utils_Array::value('user_name', $paymentProcessor));
}
/**
* @param array $paymentProcessor
*
* @return string
*/
public static function getPublicKey($paymentProcessor) {
return trim(CRM_Utils_Array::value('password', $paymentProcessor));
}
/**
* Given a payment processor id, return the public key
*
* @param $paymentProcessorId
*
* @return string
*/
public static function getPublicKeyById($paymentProcessorId) {
try {
$paymentProcessor = civicrm_api3('PaymentProcessor', 'getsingle', [
'id' => $paymentProcessorId,
]);
$key = self::getPublicKey($paymentProcessor);
}
catch (CiviCRM_API3_Exception $e) {
return '';
}
return $key;
}
/**
* Given a payment processor id, return the secret key
*
* @param $paymentProcessorId
*
* @return string
*/
public static function getSecretKeyById($paymentProcessorId) {
try {
$paymentProcessor = civicrm_api3('PaymentProcessor', 'getsingle', [
'id' => $paymentProcessorId,
]);
$key = self::getSecretKey($paymentProcessor);
}
catch (CiviCRM_API3_Exception $e) {
return '';
}
return $key;
}
/**
* This function checks to see if we have the right config values.
*
......@@ -44,14 +105,6 @@ class CRM_Core_Payment_Stripe extends CRM_Core_Payment {
public function checkConfig() {
$error = array();
if (empty($this->_paymentProcessor['user_name'])) {
$error[] = ts('The "Secret Key" is not set in the Stripe Payment Processor settings.');
}
if (empty($this->_paymentProcessor['password'])) {
$error[] = ts('The "Publishable Key" is not set in the Stripe Payment Processor settings.');
}
if (!empty($error)) {
return implode('<p>', $error);
}
......@@ -106,8 +159,8 @@ class CRM_Core_Payment_Stripe extends CRM_Core_Payment {
public function setAPIParams() {
// Set plugin info and API credentials.
\Stripe\Stripe::setAppInfo('CiviCRM', CRM_Utils_System::version(), CRM_Utils_System::baseURL());
\Stripe\Stripe::setApiKey($this->_paymentProcessor['user_name']);
\Stripe\Stripe::setApiVersion($this->_stripeAPIVersion);
\Stripe\Stripe::setApiKey(self::getSecretKey($this->_paymentProcessor));
\Stripe\Stripe::setApiVersion(self::getApiVersion());
}
/**
......@@ -341,7 +394,7 @@ class CRM_Core_Payment_Stripe extends CRM_Core_Payment {
public function buildForm(&$form) {
// Set default values
$paymentProcessorId = CRM_Utils_Array::value('id', $form->_paymentProcessor);
$publishableKey = CRM_Core_Payment_Stripe::getPublishableKey($paymentProcessorId);
$publishableKey = CRM_Core_Payment_Stripe::getPublicKeyById($paymentProcessorId);
$defaults = [
'stripe_id' => $paymentProcessorId,
'stripe_pub_key' => $publishableKey,
......@@ -349,26 +402,6 @@ class CRM_Core_Payment_Stripe extends CRM_Core_Payment {
$form->setDefaults($defaults);
}
/**
* Given a payment processor id, return the publishable key (password field)
*
* @param $paymentProcessorId
*
* @return string
*/
public static function getPublishableKey($paymentProcessorId) {
try {
$publishableKey = (string) civicrm_api3('PaymentProcessor', 'getvalue', array(
'return' => "password",
'id' => $paymentProcessorId,
));
}
catch (CiviCRM_API3_Exception $e) {
return '';
}
return $publishableKey;
}
/**
* Process payment
* Submit a payment using Stripe's PHP API:
......
......@@ -171,6 +171,7 @@ class CRM_Core_Payment_StripeIPN extends CRM_Core_Payment_BaseIPN {
$pendingStatusId = CRM_Core_PseudoConstant::getKey('CRM_Contribute_BAO_Contribution', 'contribution_status_id', 'Pending');
// NOTE: If you add an event here make sure you add it to the webhook or it will never be received!
switch($this->event_type) {
// Successful recurring payment.
case 'invoice.payment_succeeded':
......
<?php
use CRM_Stripe_ExtensionUtil as E;
class CRM_Stripe_Utils_Check_Webhook {
/**
* Checks whether the live Stripe processors have a correctly configured
* webhook (we may want to check the test processors too, at some point, but
* for now, avoid having false alerts that will annoy people).
*
* @see stripe_civicrm_check()
*/
public static function check(&$messages) {
$result = civicrm_api3('PaymentProcessor', 'get', [
'class_name' => 'Payment_Stripe',
'is_active' => 1,
'is_test' => 0,
]);
foreach ($result['values'] as $pp) {
$sk = $pp['user_name'];
$webhook_path = stripe_get_webhook_path(TRUE);
$webhook_path = str_replace('NN', $pp['id'], $webhook_path);
\Stripe\Stripe::setApiKey($sk);
try {
$webhooks = \Stripe\WebhookEndpoint::all(["limit" => 100]);
}
catch (Exception $e) {
$error = $e->getMessage();
$messages[] = new CRM_Utils_Check_Message(
'stripe_webhook',
E::ts('The %1 (%2) Payment Processor has an error: %3', [
1 => $pp['name'],
2 => $pp['id'],
3 => $error,
]),
E::ts('Stripe - API Key'),
\Psr\Log\LogLevel::ERROR,
'fa-money'
);
continue;
}
if (empty($webhooks->data)) {
$messages[] = new CRM_Utils_Check_Message(
'stripe_webhook',
E::ts('The %1 (%2) Payment Processor does not have a webhook configured. This is only required for recurring contributions. You can review from your Stripe account, under Developers > Webhooks. The webhook URL is: %3', [
1 => $pp['name'],
2 => $pp['id'],
3 => $webhook_path,
]),
E::ts('Stripe - Webhook'),
\Psr\Log\LogLevel::INFO,
'fa-money'
);
continue;
}
$found_wh = FALSE;
foreach ($webhooks->data as $wh) {
if ($wh->url == $webhook_path) {
$found_wh = TRUE;
}
}
if ($found_wh) {
$messages[] = new CRM_Utils_Check_Message(
'stripe_webhook',
E::ts('The %1 (%2) Payment Processor has a webhook configured (%3).', [
1 => $pp['name'],
2 => $pp['id'],
3 => $webhook_path,
]),
E::ts('Stripe - Webhook'),
\Psr\Log\LogLevel::INFO,
'fa-money'
);
}
else {
$messages[] = new CRM_Utils_Check_Message(
'stripe_webhook',
E::ts('The %1 (%2) Payment Processor does not have a valid webhook configured for this website. This is only required for recurring contributions. You can review from your Stripe account, under Developers > Webhooks. The webhook URL is: %3', [
1 => $pp['name'],
2 => $pp['id'],
3 => urldecode($webhook_path),
]),
E::ts('Stripe - Webhook'),
\Psr\Log\LogLevel::WARNING,
'fa-money'
);
}
}
}
}
<?php
use CRM_Stripe_ExtensionUtil as E;
class CRM_Stripe_Webhook {
use CRM_Stripe_Webhook_Trait;
/**
* Checks whether the payment processors have a correctly configured
* webhook (we may want to check the test processors too, at some point, but
* for now, avoid having false alerts that will annoy people).
*
* @see stripe_civicrm_check()
*/
public static function check() {
$result = civicrm_api3('PaymentProcessor', 'get', [
'class_name' => 'Payment_Stripe',
'is_active' => 1,
]);
foreach ($result['values'] as $paymentProcessor) {
$webhook_path = self::getWebhookPath(TRUE, $paymentProcessor['id']);
\Stripe::setApiKey(CRM_Core_Payment_Stripe::getSecretKey($paymentProcessor));
try {
$webhooks = \Stripe\WebhookEndpoint::all(["limit" => 100]);
}
catch (Exception $e) {
$error = $e->getMessage();
$messages[] = new CRM_Utils_Check_Message(
'stripe_webhook',
E::ts('The %1 (%2) Payment Processor has an error: %3', [
1 => $paymentProcessor['name'],
2 => $paymentProcessor['id'],
3 => $error,
]),
E::ts('Stripe - API Key'),
\Psr\Log\LogLevel::ERROR,
'fa-money'
);
continue;
}
$found_wh = FALSE;
foreach ($webhooks->data as $wh) {
if ($wh->url == $webhook_path) {
$found_wh = TRUE;
// Check and update webhook
self::checkAndUpdateWebhook($wh);
}
}
if (!$found_wh) {
try {
self::createWebhook($paymentProcessor['id']);
}
catch (Exception $e) {
$messages[] = new CRM_Utils_Check_Message(
'stripe_webhook',
E::ts('Could not create webhook. You can review from your Stripe account, under Developers > Webhooks.<br/>The webhook URL is: %3', [
1 => $paymentProcessor['name'],
2 => $paymentProcessor['id'],
3 => urldecode($webhook_path),
]) . '.<br/>Error from Stripe: <em>' . $e->getMessage() . '</em>',
E::ts('Stripe Webhook: %1 (%2)', [
1 => $paymentProcessor['name'],
2 => $paymentProcessor['id'],
]
),
\Psr\Log\LogLevel::WARNING,
'fa-money'
);
}
}
}
return $messages;
}
/**
* Create a new webhook for payment processor
*
* @param int $paymentProcessorId
*/
public static function createWebhook($paymentProcessorId) {
\Stripe\Stripe::setApiKey(CRM_Core_Payment_Stripe::getSecretKeyById($paymentProcessorId));
$params = [
'enabled_events' => self::getDefaultEnabledEvents(),
'url' => self::getWebhookPath(TRUE, $paymentProcessorId),
'api_version' => CRM_Core_Payment_Stripe::getApiVersion(),
'connect' => FALSE,
];
\Stripe\WebhookEndpoint::create($params);
}
/**
* Check and update existing webhook
*
* @param array $webhook
*/
public static function checkAndUpdateWebhook($webhook) {
$update = FALSE;
if ($webhook['api_version'] !== CRM_Core_Payment_Stripe::API_VERSION) {
$update = TRUE;
$params['api_version'] = CRM_Core_Payment_Stripe::API_VERSION;
}
if (array_diff(self::getDefaultEnabledEvents(), $webhook['enabled_events'])) {
$update = TRUE;
$params['enabled_events'] = self::getDefaultEnabledEvents();
}
if ($update) {
\Stripe\WebhookEndpoint::update($webhook['id'], $params);
}
}
/**
* List of webhooks we currently handle
* @return array
*/
public static function getDefaultEnabledEvents() {
return [
'invoice.payment_succeeded',
'invoice.payment_failed',
'charge.failed',
'charge.refunded',
'charge.succeeded',
'customer.subscription.updated',
'customer.subscription.deleted',
];
}
}
<?php
trait CRM_Stripe_Webhook_Trait {
/**********************
* MJW_Webhook_Trait: 20190602
*********************/
/**
* @var array Payment processor
*/
private $_paymentProcessor;
/**
* Get the path of the webhook depending on the UF (eg Drupal, Joomla, Wordpress)
*
* @param bool $includeBaseUrl
* @param string $pp_id
*
* @return string
*/
public static function getWebhookPath($includeBaseUrl = TRUE, $paymentProcessorId = 'NN') {
// Assuming frontend URL because that's how the function behaved before.
// @fixme this doesn't return the right webhook path on Wordpress (often includes an extra path between .com and ? eg. abc.com/xxx/?page=CiviCRM
// We can't use CRM_Utils_System::url('civicrm/payment/ipn/' . $paymentProcessorId, NULL, $includeBaseUrl, NULL, FALSE, TRUE);
// because it returns the query string urlencoded and the base URL non urlencoded so we can't use to match existing webhook URLs
$UFWebhookPaths = [
"Drupal" => "civicrm/payment/ipn/{$paymentProcessorId}",
"Joomla" => "?option=com_civicrm&task=civicrm/payment/ipn/{$paymentProcessorId}",
"WordPress" => "?page=CiviCRM&q=civicrm/payment/ipn/{$paymentProcessorId}"
];
$basePage = '';
$config = CRM_Core_Config::singleton();
if (!empty($config->wpBasePage) && $config->userFramework == 'WordPress') {
// Add in the wordpress base page to the URL.
$basePage = (substr($config->wpBasePage, -1) == '/') ? $config->wpBasePage : "$config->wpBasePage/";
}
// Use Drupal path as default if the UF isn't in the map above
$UFWebhookPath = (array_key_exists(CIVICRM_UF, $UFWebhookPaths)) ? $UFWebhookPaths[CIVICRM_UF] : $UFWebhookPaths['Drupal'];
if ($includeBaseUrl) {
return CRM_Utils_System::baseURL() . $basePage . $UFWebhookPath;
}
return $UFWebhookPath;
}
}
<?php
/**
* The record will be automatically inserted, updated, or deleted from the
* database as appropriate. For more details, see "hook_civicrm_managed" at:
* https://docs.civicrm.org/dev/en/latest/hooks/hook_civicrm_managed/
*/
return [
0 => [
'name' => 'Stripe',
'entity' => 'PaymentProcessorType',
'params' => [
'version' => 3,
'name' => 'Stripe',
'title' => 'Stripe',
'description' => 'Stripe Payment Processor',
'class_name' => 'Payment_Stripe',
'user_name_label' => 'Secret Key',
'password_label' => 'Public Key',
'url_site_default' => 'https://api.stripe.com/v2',
'url_recur_default' => 'https://api.stripe.com/v2',
'url_site_test_default' => 'https://api.stripe.com/v2',
'url_recur_test_default' => 'https://api.stripe.com/v2',
'billing_mode' => 1,
'payment_type' => 1,
'is_recur' => 1,
],
],
];
......@@ -39,20 +39,6 @@ function stripe_civicrm_uninstall() {
* Implementation of hook_civicrm_enable().
*/
function stripe_civicrm_enable() {
$UFWebhookPath = stripe_get_webhook_path(TRUE);
CRM_Core_Session::setStatus(
"
<br />Don't forget to set up Webhooks in Stripe so that recurring contributions are ended!
<br />Webhook path to enter in Stripe:
<br/><em>$UFWebhookPath</em>
<br />Replace NN with the actual payment processor ID configured on your site.
<br />
",
'Stripe Payment Processor',
'info',
['expires' => 0]
);
_stripe_civix_civicrm_enable();
}
......@@ -84,28 +70,6 @@ function stripe_civicrm_upgrade($op, CRM_Queue_Queue $queue = NULL) {
* is installed, disabled, uninstalled.
*/
function stripe_civicrm_managed(&$entities) {
$entities[] = array(
'module' => 'com.drastikbydesign.stripe',
'name' => 'Stripe',
'entity' => 'PaymentProcessorType',
'params' => array(
'version' => 3,
'name' => 'Stripe',
'title' => 'Stripe',
'description' => 'Stripe Payment Processor',
'class_name' => 'Payment_Stripe',
'billing_mode' => 'form',
'user_name_label' => 'Secret Key',
'password_label' => 'Publishable Key',
'url_site_default' => 'https://api.stripe.com/v2',
'url_recur_default' => 'https://api.stripe.com/v2',
'url_site_test_default' => 'https://api.stripe.com/v2',
'url_recur_test_default' => 'https://api.stripe.com/v2',
'is_recur' => 1,
'payment_type' => 1
),
);
_stripe_civix_civicrm_managed($entities);
}
......@@ -200,50 +164,9 @@ function stripe_civicrm_buildForm($formName, &$form) {
}
}
/**
* Get the path of the webhook depending on the UF (eg Drupal, Joomla, Wordpress)
*
* @param bool $includeBaseUrl
* @param string $pp_id
*
* @return string
*/
function stripe_get_webhook_path($includeBaseUrl = TRUE, $pp_id = 'NN') {
// Assuming frontend URL because that's how the function behaved before.
// @fixme this doesn't return the right webhook path on Wordpress (often includes an extra path between .com and ? eg. abc.com/xxx/?page=CiviCRM
// return CRM_Utils_System::url('civicrm/payment/ipn/' . $pp_id, NULL, $includeBaseUrl, NULL, FALSE, TRUE);
$UFWebhookPaths = [
"Drupal" => "civicrm/payment/ipn/NN",
"Joomla" => "?option=com_civicrm&task=civicrm/payment/ipn/NN",
"WordPress" => "?page=CiviCRM&q=civicrm/payment/ipn/NN"
];
// Use Drupal path as default if the UF isn't in the map above
$UFWebhookPath = (array_key_exists(CIVICRM_UF, $UFWebhookPaths)) ?
$UFWebhookPaths[CIVICRM_UF] :
$UFWebhookPaths['Drupal'];
if ($includeBaseUrl) {
$sepChar = (substr(CIVICRM_UF_BASEURL, -1) == '/') ? '' : '/';
return CIVICRM_UF_BASEURL . $sepChar . $UFWebhookPath;
}
return $UFWebhookPath;
}
/*
* Implementation of hook_idsException.
*
* Ensure webhooks don't get caught in the IDS check.
*/
function stripe_civicrm_idsException(&$skip) {
// Path is always set to civicrm/payment/ipn (checked on Drupal/Joomla)
$skip[] = 'civicrm/payment/ipn';
}
/**
* Implements hook_civicrm_check().
*/
function stripe_civicrm_check(&$messages) {
CRM_Stripe_Utils_Check_Webhook::check($messages);
$messages = CRM_Stripe_Webhook::check();
}
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment