diff --git a/.DS_Store b/.DS_Store deleted file mode 100644 index 8246bd1..0000000 Binary files a/.DS_Store and /dev/null differ 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 diff --git a/class-gf-paystack-api.php b/class-gf-paystack-api.php index 6827632..d3a5be8 100644 --- a/class-gf-paystack-api.php +++ b/class-gf-paystack-api.php @@ -1,7 +1,14 @@ $this->plugin_name, - // 'public_key' => $this->public_key, - // 'transaction_reference' => $reference - // ]; - $params = [ 'plugin_name' => 'pstk-gravityforms', 'public_key' => $this->public_key, @@ -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, @@ -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); } /** diff --git a/class-gf-paystack.php b/class-gf-paystack.php index 0627d9b..dbaca85 100644 --- a/class-gf-paystack.php +++ b/class-gf-paystack.php @@ -1,7 +1,20 @@ - - - +

+

- - - +
    +
  1. +

    +

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

    +
  2. +
  3. +

    +

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

    +
  4. +
  5. +

    +
  6. +
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')), ); } @@ -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']}-"); @@ -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']); } @@ -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']]; @@ -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) { @@ -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; } @@ -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); } @@ -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': @@ -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'; @@ -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; @@ -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, @@ -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()); @@ -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, - ]; + ); } } diff --git a/paystack.php b/paystack.php index ff17b8f..37e093a 100644 --- a/paystack.php +++ b/paystack.php @@ -32,11 +32,21 @@ 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')) { @@ -44,14 +54,53 @@ public static function load() } 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( + '

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(); }