Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Binary file removed .DS_Store
Binary file not shown.
23 changes: 23 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
# macOS
.DS_Store
.AppleDouble
.LSOverride

# Thumbnails
._*

# Files that might appear in the root of a volume
.DocumentRevisions-V100
.fseventsd
.Spotlight-V100
.TemporaryItems
.Trashes
.VolumeIcon.icns
.com.apple.timemachine.donotpresent

# Directories potentially created on remote AFP share
.AppleDB
.AppleDesktop
Network Trash Folder
Temporary Items
apdisk
40 changes: 25 additions & 15 deletions class-gf-paystack-api.php
Original file line number Diff line number Diff line change
@@ -1,7 +1,14 @@
<?php
if (!defined('ABSPATH')) {
die('You are not allowed to call this page directly.');
}
/**
* Paystack API Class for Gravity Forms
*
* @package GF_Paystack
* @author Paystack
* @copyright 2020 Paystack
* @license GPL-2.0+
*/

defined('ABSPATH') || die();

class GFPaystackApi
{
Expand All @@ -27,12 +34,6 @@ public function __construct($config)
public function log_transaction_success($reference)
{
// Send reference to logger along with plugin name and public key
// $params = [
// 'plugin_name' => $this->plugin_name,
// 'public_key' => $this->public_key,
// 'transaction_reference' => $reference
// ];

$params = [
'plugin_name' => 'pstk-gravityforms',
'public_key' => $this->public_key,
Expand All @@ -55,7 +56,8 @@ public function log_transaction_success($reference)
* @param string $method API request method
* @param string $domain API request uri
*
* @return object|null JSON decoded transaction object. NULL on API error.
* @return array JSON decoded transaction object.
* @throws Exception on API error.
*/
public function send_request(
$endpoint,
Expand Down Expand Up @@ -91,19 +93,27 @@ public function send_request(
} else { // Un-decipherable message
throw new Exception(sprintf(__('There was an issue connecting with the payment processor. Try again later.', 'gravityformspaystack'), $this->name));
}

return false;
}

/**
* Validate Webhook Signature
*
* @param $input
* @return boolean
* Uses hash_equals() for timing-safe comparison to prevent timing attacks.
*
* @param string $input Raw request body
* @return boolean True if signature is valid, false otherwise
*/
public function validate_webhook($input)
{
return $_SERVER['HTTP_X_PAYSTACK_SIGNATURE'] == hash_hmac('sha512', $input, $this->secret_key);
$signature = isset($_SERVER['HTTP_X_PAYSTACK_SIGNATURE']) ? $_SERVER['HTTP_X_PAYSTACK_SIGNATURE'] : '';

if (empty($signature)) {
return false;
}

$expected = hash_hmac('sha512', $input, $this->secret_key);

return hash_equals($expected, $signature);
}

/**
Expand Down
95 changes: 54 additions & 41 deletions class-gf-paystack.php
Original file line number Diff line number Diff line change
@@ -1,7 +1,20 @@
<?php
/**
* Paystack Add-On for Gravity Forms
*
* @package GF_Paystack
* @author Paystack
* @copyright 2020 Paystack
* @license GPL-2.0+
*/

defined('ABSPATH') || die();

// Safety check: ensure Gravity Forms is loaded before including the framework.
if (!class_exists('GFForms') || !method_exists('GFForms', 'include_payment_addon_framework')) {
return;
}

// Include the payment add-on framework.
GFForms::include_payment_addon_framework();

Expand Down Expand Up @@ -377,28 +390,22 @@ public function get_webhooks_section_description()
{
ob_start();
?>
<a href="javascript:void(0);" onclick="tb_show('Webhook Instructions', '#TB_inline?width=500&inlineId=paystack-webhooks-instructions', '');" onkeypress="tb_show('Webhook Instructions', '#TB_inline?width=500&inlineId=paystack-webhooks-instructions', '');">
<?php esc_html_e('View Instructions', 'gravityformspaystack'); ?>
</a>
<p class="paystack-webhooks-description">
<?php esc_html_e('Configure webhooks to receive payment notifications:', 'gravityformspaystack'); ?>
</p>

<div id="paystack-webhooks-instructions" style="display:none;">
<ol class="paystack-webhooks-instructions">
<li>
<p><?php esc_html_e('Click the following link and log in to access your Paystack Webhooks management page:', 'gravityformspaystack'); ?> </p>
<a href="https://dashboard.paystack.com/#/settings/developer" target="_blank">https://dashboard.paystack.com/#/settings/developer</a>
</li>
<li>
<p>
<?php esc_html_e('Enter the following URL in the "Webhook URL" field:', 'gravityformspaystack'); ?>
<br />
<code><?php echo $this->get_webhook_url($this->get_current_feed_id()); ?></code>
</p>
</li>
<li><?php esc_html_e('Save Changes.', 'gravityformspaystack'); ?></li>
</ol>
</div>

<ol class="paystack-webhooks-instructions" style="margin-left: 20px;">
<li>
<p><?php esc_html_e('Click the following link and log in to access your Paystack Webhooks management page:', 'gravityformspaystack'); ?></p>
<p><a href="https://dashboard.paystack.com/#/settings/developer" target="_blank" rel="noopener noreferrer">https://dashboard.paystack.com/#/settings/developer</a></p>
</li>
<li>
<p><?php esc_html_e('Enter the following URL in the "Webhook URL" field:', 'gravityformspaystack'); ?></p>
<p><code style="display: inline-block; padding: 8px 12px; background: #f0f0f1; border-radius: 4px; word-break: break-all;"><?php echo esc_url($this->get_webhook_url($this->get_current_feed_id())); ?></code></p>
</li>
<li>
<p><?php esc_html_e('Save Changes.', 'gravityformspaystack'); ?></p>
</li>
</ol>
<?php
return ob_get_clean();
}
Expand Down Expand Up @@ -623,13 +630,15 @@ public function settings_billing_cycle($field, $echo = true)
*/
public function supported_billing_intervals()
{
// Intervals supported by Paystack API: hourly, daily, weekly, monthly, quarterly, biannually, annually
return array(
'hourly' => array('label' => esc_html__('Hourly', 'gravityformspaystack')),
'daily' => array('label' => esc_html__('Daily', 'gravityformspaystack')),
'weekly' => array('label' => esc_html__('Weekly', 'gravityformspaystack')),
'monthly' => array('label' => esc_html__('Monthly', 'gravityformspaystack')),
'annually' => array('label' => esc_html__('Annually', 'gravityformspaystack')),
'hourly' => array('label' => esc_html__('Hourly', 'gravityformspaystack')),
'daily' => array('label' => esc_html__('Daily', 'gravityformspaystack')),
'weekly' => array('label' => esc_html__('Weekly', 'gravityformspaystack')),
'monthly' => array('label' => esc_html__('Monthly', 'gravityformspaystack')),
'quarterly' => array('label' => esc_html__('Quarterly', 'gravityformspaystack')),
'biannually' => array('label' => esc_html__('Biannually', 'gravityformspaystack')),
'annually' => array('label' => esc_html__('Annually', 'gravityformspaystack')),
);
}

Expand Down Expand Up @@ -933,11 +942,11 @@ public function redirect_url($feed, $submission_data, $form, $entry)
// 'value' => $this->paystack_api->plugin_name
// ];

$custom_data[] = [
$custom_data[] = array(
'display_name' => 'Plugin Name',
'variable_name' => 'plugin_name',
'value' => 'pstk-gravityforms'
];
);

// Generate transaction reference
$reference = uniqid("gf-{$entry['id']}-");
Expand Down Expand Up @@ -984,7 +993,7 @@ public function redirect_url($feed, $submission_data, $form, $entry)
$args['invoice_limit'] = (int) $invoice_limit;
}

$args['channels'] = ['card'];
$args['channels'] = array('card');

gform_update_meta($entry['id'], 'paystack_plan_code', $plan['plan_code']);
}
Expand All @@ -1009,7 +1018,7 @@ public function redirect_url($feed, $submission_data, $form, $entry)

public function get_fields_meta_data($feed, $entry, $fields)
{
$data = [];
$data = array();

foreach ($fields as $field) {
$field_id = $feed['meta'][$field['meta_name']];
Expand Down Expand Up @@ -1160,7 +1169,7 @@ public function maybe_thankyou_page()
$reference = sanitize_text_field(rgget('reference'));

try {
$response = $this->paystack_api->send_request("transaction/verify/{$reference}", [], 'get');
$response = $this->paystack_api->send_request("transaction/verify/{$reference}", array(), 'get');

$this->log_debug(__METHOD__ . "(): Transaction verified. " . print_r($response, 1));
} catch (\Exception $e) {
Expand Down Expand Up @@ -1224,6 +1233,9 @@ public function maybe_thankyou_page()
*/
public function callback()
{
// Initialize action array to prevent undefined variable errors
$action = array();

if (!$this->is_gravityforms_supported()) {
return;
}
Expand All @@ -1245,9 +1257,10 @@ public function callback()
return false;
}

$entry_id = rgars($event, 'data/metadata/entry_id');
// Sanitize entry_id from webhook metadata
$entry_id = absint(rgars($event, 'data/metadata/entry_id'));

if (!$entry_id && $reference = rgars($event, 'data/reference')) {
if (!$entry_id && $reference = sanitize_text_field(rgars($event, 'data/reference'))) {
$entry_id = $this->get_entry_id_by_reference($reference);
}

Expand Down Expand Up @@ -1330,13 +1343,13 @@ public function callback()
$action['payment_method'] = $this->_slug;
$action['ready_to_fulfill'] = !$entry['is_fulfilled'] ? true : false;

$action['add_subscription_payment'] = [
$action['add_subscription_payment'] = array(
'entry_id' => $entry_id,
'subscription_id' => rgars($subscription, 'data/subscription_code'),
'transaction_id' => rgar($transaction, 'id'),
'amount' => $this->get_amount_import(rgar($transaction, 'amount'), rgar($entry, 'currency')),
'payment_method' => $this->_slug
];
);

break;
case 'subscription.disable':
Expand Down Expand Up @@ -1429,7 +1442,7 @@ public function callback()

$transaction = $subscription['data']['invoices'][$key];
} else {
$transaction = $this->paystack_api->send_request("transaction/verify/{$reference}", [], 'get');
$transaction = $this->paystack_api->send_request("transaction/verify/{$reference}", array(), 'get');
}

$action['type'] = 'add_subscription_payment';
Expand Down Expand Up @@ -1581,7 +1594,7 @@ public function is_webhook_enabled()
public function get_plan($plan_id_or_code)
{
// Get Paystack plan.
$response = (object) $this->paystack_api->send_request("plan/{$plan_id_or_code}", [], 'get');
$response = (object) $this->paystack_api->send_request("plan/{$plan_id_or_code}", array(), 'get');

$plan = $response->data;

Expand Down Expand Up @@ -1612,7 +1625,7 @@ public function create_plan($feed, $payment_amount, $currency)
$amount = $this->get_amount_export($payment_amount, $currency);

$recurring_times = (int) rgar($feed['meta'], 'recurringTimes');
$send_invoices = (int) rgar($feed['meta'], 'sendInvoices') == 1 ? 'true' : 'false';
$send_invoices = (bool) rgar($feed['meta'], 'sendInvoices');

$args = array(
'name' => $name,
Expand Down Expand Up @@ -1662,7 +1675,7 @@ public function get_subscription($subscription_id_or_code)
$this->log_debug(__METHOD__ . '(): Getting subscription ' . $subscription_id_or_code);

try {
$subscription = $this->paystack_api->send_request("subscription/{$subscription_id_or_code}", [], 'get');
$subscription = $this->paystack_api->send_request("subscription/{$subscription_id_or_code}", array(), 'get');
} catch (\Exception $e) {
$this->log_error(__METHOD__ . '(): Unable to get subscription. Reason: ' . $e->getMessage());

Expand Down Expand Up @@ -1713,11 +1726,11 @@ public function get_paystack_meta_data($feed, $entry, $form)
$field_value = substr($field_value, 0, 500);

// Add to metadata array.
$metadata[] = [
$metadata[] = array(
'display_name' => $meta['custom_key'],
'variable_name' => sanitize_title($meta['custom_key']),
'value' => $field_value,
];
);
}
}

Expand Down
51 changes: 50 additions & 1 deletion paystack.php
Original file line number Diff line number Diff line change
Expand Up @@ -32,26 +32,75 @@
defined('ABSPATH') || die();

define('GF_PAYSTACK_VERSION', '2.0.6');
define('GF_PAYSTACK_MIN_GF_VERSION', '2.0');

// Register the addon - only runs if Gravity Forms is loaded
add_action('gform_loaded', array('GF_Paystack_Bootstrap', 'load'), 5);

// Show admin notice if Gravity Forms is not active
add_action('admin_notices', array('GF_Paystack_Bootstrap', 'admin_notice'));

class GF_Paystack_Bootstrap
{
/**
* Load the add-on if Gravity Forms is available.
*
* @return void
*/
public static function load()
{
if (!method_exists('GFForms', 'include_payment_addon_framework')) {
return;
}

require_once('class-gf-paystack.php');

require_once('class-gf-paystack-api.php');

GFAddOn::register('GFPaystack');
}

/**
* Display admin notice if Gravity Forms is not active.
*
* @return void
*/
public static function admin_notice()
{
// Only show on plugins page or dashboard
$screen = get_current_screen();
if (!$screen || !in_array($screen->id, array('plugins', 'plugins-network', 'dashboard'))) {
return;
}

// Check if Gravity Forms is active
if (!class_exists('GFForms')) {
printf(
'<div class="notice notice-error"><p><strong>Paystack for Gravity Forms</strong> requires Gravity Forms to be installed and active. <a href="%s" target="_blank" rel="noopener noreferrer">Get Gravity Forms</a></p></div>',
esc_url('https://www.gravityforms.com/')
);
return;
}

// Check if minimum version requirement is met
if (method_exists('GFForms', 'version') && version_compare(GFForms::version(), GF_PAYSTACK_MIN_GF_VERSION, '<')) {
printf(
'<div class="notice notice-error"><p><strong>Paystack for Gravity Forms</strong> requires Gravity Forms %s or higher. Please update Gravity Forms.</p></div>',
esc_html(GF_PAYSTACK_MIN_GF_VERSION)
);
return;
}
}
}

/**
* Returns an instance of the GFPaystack class.
*
* @return GFPaystack|false
*/
function gf_paystack()
{
if (!class_exists('GFPaystack')) {
return false;
}
return GFPaystack::get_instance();
}