From 10546a3fb1984202693b82b8e0e2b972b8ff691b Mon Sep 17 00:00:00 2001 From: K2SO Date: Wed, 12 Aug 2026 09:39:42 +0200 Subject: [PATCH 1/5] Security and logic fixes for Paystack Gravity Forms integration Critical fixes: - Use hash_equals() for webhook signature validation (prevents timing attacks) - Initialize $action as array() in callback() to prevent undefined variable errors - Sanitize entry_id from webhook metadata using absint() - Sanitize reference strings using sanitize_text_field() API compliance fixes: - Remove unsupported billing intervals (hourly, biannually) - Add quarterly billing interval per Paystack API spec - Change send_invoices from string to boolean type WordPress coding standards: - Use array() syntax instead of [] (WordPress coding standards) - Add proper docblocks with @throws annotations --- class-gf-paystack-api.php | 27 ++++++++++++++------------ class-gf-paystack.php | 40 +++++++++++++++++++++------------------ 2 files changed, 37 insertions(+), 30 deletions(-) diff --git a/class-gf-paystack-api.php b/class-gf-paystack-api.php index 6827632..feb9b9b 100644 --- a/class-gf-paystack-api.php +++ b/class-gf-paystack-api.php @@ -27,12 +27,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, @@ -55,7 +49,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, @@ -91,19 +86,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); } /** diff --git a/class-gf-paystack.php b/class-gf-paystack.php index 0627d9b..21d137c 100644 --- a/class-gf-paystack.php +++ b/class-gf-paystack.php @@ -623,13 +623,13 @@ public function settings_billing_cycle($field, $echo = true) */ public function supported_billing_intervals() { + // Only return intervals supported by Paystack API: daily, weekly, monthly, quarterly, 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')), - 'biannually' => array('label' => esc_html__('Biannually', 'gravityformspaystack')), + 'quarterly' => array('label' => esc_html__('Quarterly', 'gravityformspaystack')), + 'annually' => array('label' => esc_html__('Annually', 'gravityformspaystack')), ); } @@ -933,11 +933,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']}-"); @@ -984,7 +984,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']); } @@ -1009,7 +1009,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']]; @@ -1160,7 +1160,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) { @@ -1224,6 +1224,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; } @@ -1245,9 +1248,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); } @@ -1330,13 +1334,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': @@ -1429,7 +1433,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'; @@ -1581,7 +1585,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; @@ -1612,7 +1616,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, @@ -1662,7 +1666,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()); @@ -1713,11 +1717,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, - ]; + ); } } From 2f81d17583043b6ed6c85dc0acb1e6703d22b9e8 Mon Sep 17 00:00:00 2001 From: K2SO Date: Wed, 12 Aug 2026 09:47:30 +0200 Subject: [PATCH 2/5] Add Gravity Forms dependency check to prevent fatal errors - Add GF_PAYSTACK_MIN_GF_VERSION constant for minimum version requirement - Add admin_notice() to show error if Gravity Forms is not installed - Add safety check in class-gf-paystack.php to return early if GF not loaded - Update gf_paystack() helper to return false if class doesn't exist - Add proper file docblocks to all PHP files (WordPress coding standards) - Only show admin notice on plugins page or dashboard Prevents fatal errors when plugin is activated without Gravity Forms. --- class-gf-paystack-api.php | 13 +++++++--- class-gf-paystack.php | 13 ++++++++++ paystack.php | 51 ++++++++++++++++++++++++++++++++++++++- 3 files changed, 73 insertions(+), 4 deletions(-) diff --git a/class-gf-paystack-api.php b/class-gf-paystack-api.php index feb9b9b..d3a5be8 100644 --- a/class-gf-paystack-api.php +++ b/class-gf-paystack-api.php @@ -1,7 +1,14 @@ id, array('plugins', 'plugins-network', 'dashboard'))) { + return; + } + + // Check if Gravity Forms is active + if (!class_exists('GFForms')) { + printf( + '

Paystack for Gravity Forms requires Gravity Forms to be installed and active. Get Gravity Forms

', + 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( + '

Paystack for Gravity Forms requires Gravity Forms %s or higher. Please update Gravity Forms.

', + 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(); } From 3a4bf16789c3809b6c0c27f2fa52f057cc88d6da Mon Sep 17 00:00:00 2001 From: K2SO Date: Wed, 12 Aug 2026 10:30:34 +0200 Subject: [PATCH 3/5] Fix billing intervals to match Paystack API spec Paystack supports all these intervals: - hourly - daily - weekly - monthly - quarterly - biannually (every 6 months) - annually Previous commit incorrectly removed hourly and biannually. --- class-gf-paystack.php | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/class-gf-paystack.php b/class-gf-paystack.php index bf1ade8..6ed46a1 100644 --- a/class-gf-paystack.php +++ b/class-gf-paystack.php @@ -636,13 +636,15 @@ public function settings_billing_cycle($field, $echo = true) */ public function supported_billing_intervals() { - // Only return intervals supported by Paystack API: daily, weekly, monthly, quarterly, annually + // Intervals supported by Paystack API: hourly, daily, weekly, monthly, quarterly, biannually, annually return array( - 'daily' => array('label' => esc_html__('Daily', 'gravityformspaystack')), - 'weekly' => array('label' => esc_html__('Weekly', 'gravityformspaystack')), - 'monthly' => array('label' => esc_html__('Monthly', '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')), - 'annually' => array('label' => esc_html__('Annually', 'gravityformspaystack')), + 'biannually' => array('label' => esc_html__('Biannually', 'gravityformspaystack')), + 'annually' => array('label' => esc_html__('Annually', 'gravityformspaystack')), ); } From 73799e38159bd16df42587bb510d25aca9563b1b Mon Sep 17 00:00:00 2001 From: K2SO Date: Wed, 12 Aug 2026 11:02:04 +0200 Subject: [PATCH 4/5] Remove .DS_Store and add .gitignore for macOS files --- .DS_Store | Bin 6148 -> 0 bytes .gitignore | 23 +++++++++++++++++++++++ 2 files changed, 23 insertions(+) delete mode 100644 .DS_Store create mode 100644 .gitignore diff --git a/.DS_Store b/.DS_Store deleted file mode 100644 index 8246bd13abe5eeadaf2df8c007f75302d45d165f..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 6148 zcmeHKy-ve05WXuZ1i{de4etn~Zj7M{FF;?Ql%f`?Q7Sf}L#)Ux@fc8`24uT_DRE^J#y0Q5K6v zq#FJz1M=(=I-)6E(M|OHlEeMFuPF0;JTJ$vr7y3~w{N4zlh{A`*1wq5-i_`9QfcdR zUz*X9#?kK7J!?kPYj{7XYFJyRC|8xVM@99x_n$VwK|$x>;*zeTYWV&b)kOY!El+n< zEo(8WO4?$gI@n?x6=gsfPzHXS0rYH4ANga^Vt73fnr95L9Z!)`)c7SMY1>BQmi!C_|(Clq$OV}0Yoi8GHn zDg(+u%)o}f>`DJWZ9o6VNqVIWC9qzrf{I9-^;o4~;#x6c er4{c%oxpBn0?YzhkFY@WN5IjbgEH`|4154jEN#L7 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..4f7599c --- /dev/null +++ b/.gitignore @@ -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 From 7d246ad481c76f159ffc933645ad148fe4aec5c5 Mon Sep 17 00:00:00 2001 From: K2SO Date: Wed, 12 Aug 2026 11:40:51 +0200 Subject: [PATCH 5/5] Show webhook instructions inline instead of popup - Remove ThickBox popup (tb_show) that wasn't working - Display instructions directly inline on settings page - Add proper styling for webhook URL display - Add rel='noopener noreferrer' for security - Escape webhook URL output with esc_url() --- class-gf-paystack.php | 36 +++++++++++++++--------------------- 1 file changed, 15 insertions(+), 21 deletions(-) diff --git a/class-gf-paystack.php b/class-gf-paystack.php index 6ed46a1..dbaca85 100644 --- a/class-gf-paystack.php +++ b/class-gf-paystack.php @@ -390,28 +390,22 @@ public function get_webhooks_section_description() { ob_start(); ?> - - - +

+

- - - +
    +
  1. +

    +

    https://dashboard.paystack.com/#/settings/developer

    +
  2. +
  3. +

    +

    get_webhook_url($this->get_current_feed_id())); ?>

    +
  4. +
  5. +

    +
  6. +