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
21 changes: 15 additions & 6 deletions inc/gateways/class-base-gateway.php
Original file line number Diff line number Diff line change
Expand Up @@ -531,9 +531,12 @@
* @since 2.0.0
*
* @param string $gateway_payment_id The gateway payment id.
* @return void|string
* @return string
*/
public function get_payment_url_on_gateway($gateway_payment_id) {}
public function get_payment_url_on_gateway($gateway_payment_id): string {

Check warning on line 536 in inc/gateways/class-base-gateway.php

View workflow job for this annotation

GitHub Actions / Code Quality Checks

The method parameter $gateway_payment_id is never used

return '';
}

/**
* Returns the external link to view the membership on the membership gateway.
Expand All @@ -543,9 +546,12 @@
* @since 2.0.0
*
* @param string $gateway_subscription_id The gateway subscription id.
* @return void|string.
* @return string
*/
public function get_subscription_url_on_gateway($gateway_subscription_id) {}
public function get_subscription_url_on_gateway($gateway_subscription_id): string {

Check warning on line 551 in inc/gateways/class-base-gateway.php

View workflow job for this annotation

GitHub Actions / Code Quality Checks

The method parameter $gateway_subscription_id is never used

return '';
}

/**
* Returns the external link to view the membership on the membership gateway.
Expand All @@ -555,9 +561,12 @@
* @since 2.0.0
*
* @param string $gateway_customer_id The gateway customer id.
* @return void|string.
* @return string
*/
public function get_customer_url_on_gateway($gateway_customer_id) {}
public function get_customer_url_on_gateway($gateway_customer_id): string {

Check warning on line 566 in inc/gateways/class-base-gateway.php

View workflow job for this annotation

GitHub Actions / Code Quality Checks

The method parameter $gateway_customer_id is never used

return '';
}

/**
* Reflects membership changes on the gateway.
Expand Down
300 changes: 300 additions & 0 deletions inc/gateways/class-base-paypal-gateway.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,300 @@
<?php
/**
* Base PayPal Gateway.
*
* Base class for PayPal payment gateways. Should be extended by specific PayPal implementations.
* Follows the same pattern as Base_Stripe_Gateway for consistency.
*
* @package WP_Ultimo
* @subpackage Gateways
* @since 2.0.0
*/

namespace WP_Ultimo\Gateways;

// Exit if accessed directly
defined('ABSPATH') || exit;

/**
* Base PayPal Gateway class. Should be extended by PayPal gateway implementations.
*
* @since 2.0.0
*/
abstract class Base_PayPal_Gateway extends Base_Gateway {

/**
* Allow gateways to declare multiple additional ids.
*
* These ids can be retrieved alongside the main id,
* via the method get_all_ids().
*
* This allows hooks to work for both legacy and modern PayPal implementations.
*
* @since 2.0.0
* @var array
*/
protected $other_ids = ['paypal', 'paypal-rest'];

/**
* Partner Attribution ID (BN Code) for PayPal Partner Program tracking.
*
* This code identifies Ultimate Multisite as the integration partner
* and enables partner revenue sharing and analytics.
*
* Apply for an official BN code at: https://www.paypal.com/partnerprogram
*
* @since 2.0.0
* @var string
*/
protected $bn_code = 'UltimateMultisite_SP_PPCP';

/**
* Holds if we are in test mode.
*
* @since 2.0.0
* @var bool
*/
protected $test_mode = true;

/**
* Declares support to recurring payments.
*
* @since 2.0.0
* @return bool
*/
public function supports_recurring(): bool {

return true;
}

/**
* Declares support to subscription amount updates.
*
* @since 2.0.0
* @return bool
*/
public function supports_amount_update(): bool {

return true;
}

/**
* Returns the PayPal base URL based on test mode.
*
* @since 2.0.0
* @return string
*/
protected function get_paypal_base_url(): string {

return $this->test_mode ? 'https://www.sandbox.paypal.com' : 'https://www.paypal.com';
}

/**
* Returns the PayPal API base URL based on test mode.
*
* @since 2.0.0
* @return string
*/
protected function get_api_base_url(): string {

return $this->test_mode ? 'https://api-m.sandbox.paypal.com' : 'https://api-m.paypal.com';
}

/**
* Get the subscription description.
*
* @since 2.0.0
*
* @param \WP_Ultimo\Checkout\Cart $cart The cart object.
* @return string
*/
protected function get_subscription_description($cart): string {

$descriptor = $cart->get_cart_descriptor();

$desc = html_entity_decode(substr($descriptor, 0, 127), ENT_COMPAT, 'UTF-8');

return $desc;
}

/**
* Returns the external link to view the payment on the payment gateway.
*
* Return an empty string to hide the link element.
*
* @since 2.0.0
*
* @param string $gateway_payment_id The gateway payment id.
* @return string
*/
public function get_payment_url_on_gateway($gateway_payment_id): string {

if (empty($gateway_payment_id)) {
return '';
}

$sandbox_prefix = $this->test_mode ? 'sandbox.' : '';

return sprintf(
'https://www.%spaypal.com/activity/payment/%s',
$sandbox_prefix,
$gateway_payment_id
);
}

/**
* Returns the external link to view the subscription on PayPal.
*
* Return an empty string to hide the link element.
*
* @since 2.0.0
*
* @param string $gateway_subscription_id The gateway subscription id.
* @return string
*/
public function get_subscription_url_on_gateway($gateway_subscription_id): string {

if (empty($gateway_subscription_id)) {
return '';
}

$sandbox_prefix = $this->test_mode ? 'sandbox.' : '';

// Check if this is a REST API subscription ID (starts with I-) or legacy NVP profile ID
if (str_starts_with($gateway_subscription_id, 'I-')) {
// REST API subscription
return sprintf(
'https://www.%spaypal.com/billing/subscriptions/%s',
$sandbox_prefix,
$gateway_subscription_id
);
}

// Legacy NVP recurring payment profile
$base_url = 'https://www.%spaypal.com/us/cgi-bin/webscr?cmd=_profile-recurring-payments&encrypted_profile_id=%s';

return sprintf($base_url, $sandbox_prefix, $gateway_subscription_id);
}
Comment on lines +164 to +177
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Check composer.json for PHP version requirement
echo "=== composer.json PHP requirement ==="
if [ -f composer.json ]; then
  cat composer.json | jq '.require.php // .config."platform"."php" // empty' 2>/dev/null || grep -i '"php"' composer.json
else
  echo "No composer.json found"
fi

echo ""
echo "=== Checking README or plugin header for version info ==="
head -50 README.md 2>/dev/null | grep -i "php\|version" || echo "No README found"

echo ""
echo "=== Checking main plugin file for version requirements ==="
fd "*.php" -x grep -l "Requires PHP\|requires_php" {} \; 2>/dev/null | head -5

echo ""
echo "=== Checking if str_starts_with polyfill exists ==="
rg "function str_starts_with|if.*str_starts_with" -A 2 -B 2

echo ""
echo "=== Checking all uses of str_starts_with in codebase ==="
rg "str_starts_with" --type php -n

Repository: Multisite-Ultimate/ultimate-multisite

Length of output: 22048


str_starts_with() is incompatible with PHP 7.4.1 (the minimum supported version per composer.json).

The function str_starts_with() (used in lines 164 and 191) requires PHP 8.0+. Since the plugin supports PHP 7.4.1 and no polyfill exists in the codebase, this will cause a fatal error on any PHP 7.4 installation. This affects not only the PayPal gateway but also 28 other files throughout the codebase using the same function.

Either update the minimum PHP version requirement to 8.0+, add a polyfill for str_starts_with(), or replace the function call with PHP 7.4-compatible alternatives (e.g., strpos() === 0).

🤖 Prompt for AI Agents
In @inc/gateways/class-base-paypal-gateway.php around lines 164 - 177, Replace
usages of the PHP 8-only function str_starts_with (e.g., the check in
class-base-paypal-gateway.php that tests $gateway_subscription_id) with a PHP
7.4-compatible check: use strpos($gateway_subscription_id, 'I-') === 0 (or an
equivalent substr/strncmp) to detect the prefix; apply the same replacement to
the other ~28 occurrences across the codebase or alternatively add a simple
polyfill for str_starts_with if you prefer to keep the original calls, but do
not change composer PHP requirement unless intentionally bumping minimum PHP to
8.0.


/**
* Returns whether a gateway subscription ID is from the REST API.
*
* REST API subscription IDs start with "I-" prefix.
*
* @since 2.0.0
*
* @param string $subscription_id The subscription ID to check.
* @return bool
*/
protected function is_rest_subscription_id(string $subscription_id): bool {

return str_starts_with($subscription_id, 'I-');
}

/**
* Adds partner attribution to API request headers.
*
* This should be called when making REST API requests to PayPal
* to ensure partner tracking and revenue sharing.
*
* @since 2.0.0
*
* @param array $headers Existing headers array.
* @return array Headers with partner attribution added.
*/
protected function add_partner_attribution_header(array $headers): array {

$headers['PayPal-Partner-Attribution-Id'] = $this->bn_code;

return $headers;
}

/**
* Log a PayPal-related message.
*
* @since 2.0.0
*
* @param string $message The message to log.
* @param string $level Log level (default: 'info').
* @return void
*/
protected function log(string $message, string $level = 'info'): void {

wu_log_add('paypal', $message, $level);
}

/**
* Adds the necessary hooks for PayPal gateways.
*
* Child classes should call parent::hooks() and add their own hooks.
*
* @since 2.0.0
* @return void
*/
public function hooks(): void {

// Add admin links to PayPal for membership management
add_filter('wu_element_get_site_actions', [$this, 'add_site_actions'], 10, 4);
}

/**
* Adds PayPal-related actions to the site actions.
*
* Allows viewing subscription on PayPal for connected memberships.
*
* @since 2.0.0
*
* @param array $actions The site actions.
* @param array $atts The widget attributes.
* @param \WP_Ultimo\Models\Site $site The current site object.
* @param \WP_Ultimo\Models\Membership $membership The current membership object.
* @return array
*/
public function add_site_actions($actions, $atts, $site, $membership) {

if (! $membership) {
return $actions;
}

$payment_gateway = $membership->get_gateway();

if (! in_array($payment_gateway, $this->other_ids, true)) {
return $actions;
}

$subscription_id = $membership->get_gateway_subscription_id();

if (empty($subscription_id)) {
return $actions;
}

$subscription_url = $this->get_subscription_url_on_gateway($subscription_id);

if (! empty($subscription_url)) {
$actions['view_on_paypal'] = [
'label' => __('View on PayPal', 'ultimate-multisite'),
'icon_classes' => 'dashicons-wu-paypal wu-align-middle',
'href' => $subscription_url,
'target' => '_blank',
];
}

return $actions;
}

/**
* Checks if PayPal is properly configured.
*
* @since 2.0.0
* @return bool
*/
abstract public function is_configured(): bool;

/**
* Returns the connection status for display in settings.
*
* @since 2.0.0
* @return array{connected: bool, message: string, details: array}
*/
abstract public function get_connection_status(): array;
}
Loading
Loading