diff --git a/.distignore b/.distignore index a88ffed..ca71997 100644 --- a/.distignore +++ b/.distignore @@ -2,12 +2,19 @@ /.github /.wordpress-org -.distignore -.gitignore -CODE-OF-CONDUCT.md -CONTRIBUTING.md -ISSUE_TEMPLATE.md -LICENSE -PULL_REQUEST_TEMPLATE.md -README.md -SECURITY.md \ No newline at end of file +/.phpstan +/stubs +/vendor +/phpstan.neon +/var +/composer.json +/composer.lock + +/.gitignore +/CODE-OF-CONDUCT.md +/CONTRIBUTING.md +/ISSUE_TEMPLATE.md +/LICENSE +/PULL_REQUEST_TEMPLATE.md +/README.md +/SECURITY.md \ No newline at end of file diff --git a/.gitignore b/.gitignore index ebf869b..733156c 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,9 @@ .idea .vscode -.DS_Store \ No newline at end of file +.DS_Store +/vendor/* +!/vendor/composer +!/vendor/autoload.php + +/var +/composer.lock \ No newline at end of file diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 72bb87e..9cd522e 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -14,10 +14,12 @@ We use github to host code, to track issues and feature requests, as well as acc Pull requests are the best way to propose changes to the codebase. We actively welcome your pull requests: 1. Fork the repo and create your branch from `main`. -2. If you've added code that should be tested, add tests. -3. Ensure the test suite passes. -4. Make sure your code lints. -5. Issue that pull request! +2. Install composer dependancies +3. If you've added code that should be tested, add tests. +4. Run ./vendor/bin/phpstan and correct if necessary +5. Ensure the test suite passes. +6. Make sure your code lints. +7. Issue that pull request! ## Any contributions you make will be under the GPLv3 Software License In short, when you submit code changes, your submissions are understood to be under the same [GPLv3](LICENSE) that covers the project. diff --git a/block/helloasso-woocommerce-blocks.php b/block/helloasso-woocommerce-blocks.php index ebf74bc..ebf61b4 100644 --- a/block/helloasso-woocommerce-blocks.php +++ b/block/helloasso-woocommerce-blocks.php @@ -1,4 +1,6 @@ settings = get_option('woocommerce_helloasso_settings', []); - $this->gateway =WC_Payment_Gateways::instance()->payment_gateways()[$this->name]; + $this->gateway = \WC_Payment_Gateways::instance()->payment_gateways()[$this->name]; } diff --git a/changelog.txt b/changelog.txt index e3ae38c..311e41c 100644 --- a/changelog.txt +++ b/changelog.txt @@ -1,4 +1,12 @@ *** HelloAsso Payments for WooCommerce Changelog *** +2026-06-03 - version 1.1.3 +* Update logout message on admin panel +* Add phpstan to ensuire compatibility with wordpress and woocommerce +* PHP coverage until 8.5 +* ensure compatibility with woocommerce 10.8.1 +* ensure compatibility with wordpress 7.0 +2026-04-23 - version 1.1.2 +* Rework api calls 2024-02-28 - version 1.0.0 * Initial release diff --git a/composer.json b/composer.json index ac2de12..393db68 100644 --- a/composer.json +++ b/composer.json @@ -12,5 +12,8 @@ "name": "helloasso" } ], - "require": {} + "require-dev": { + "phpstan/phpstan": "^2.2", + "szepeviktor/phpstan-wordpress": "^2.0" + } } diff --git a/cron/helloasso-woocommerce-cron.php b/cron/helloasso-woocommerce-cron.php index bf1c6a8..463f620 100644 --- a/cron/helloasso-woocommerce-cron.php +++ b/cron/helloasso-woocommerce-cron.php @@ -1,7 +1,5 @@ access_token)) { + if (is_object($data) && isset($data->access_token)) { update_option('helloasso_access_token_asso', $data->access_token); update_option('helloasso_refresh_token_asso', $data->refresh_token); update_option('helloasso_token_expires_in_asso', time() + $data->expires_in); diff --git a/helloasso-api/helloasso-woocommerce-api.php b/helloasso-api/helloasso-woocommerce-api.php index 8078f7f..93d6a2d 100644 --- a/helloasso-api/helloasso-woocommerce-api.php +++ b/helloasso-api/helloasso-woocommerce-api.php @@ -1,11 +1,9 @@ substr($access_token, 0, 10) . '...', 'expires_in' => $token_expires_in - time() @@ -26,7 +27,7 @@ function helloasso_get_oauth_token($client_id, $client_secret, $api_url) return $access_token; } - if ($refresh_token && time() < $refresh_token_expires_in) { + if (is_string($refresh_token) && time() < $refresh_token_expires_in) { helloasso_log_info('Rafraîchissement du token OAuth2', array( 'refresh_token_preview' => substr($refresh_token, 0, 10) . '...' )); @@ -52,6 +53,8 @@ function helloasso_get_oauth_token($client_id, $client_secret, $api_url) $response_body = wp_remote_retrieve_body($response); $response_code = wp_remote_retrieve_response_code($response); + + /** @var object{access_token?: string, refresh_token?: string, expires_in?: int|string} $data */ $data = json_decode($response_body); helloasso_log_info('Réponse rafraîchissement token OAuth2', array( @@ -59,15 +62,19 @@ function helloasso_get_oauth_token($client_id, $client_secret, $api_url) 'has_access_token' => isset($data->access_token) )); - if (isset($data->access_token)) { + if (isset($data->access_token) && is_string($data->access_token)) { update_option('helloasso_access_token', $data->access_token); - update_option('helloasso_refresh_token', $data->refresh_token); - update_option('helloasso_token_expires_in', time() + $data->expires_in); + if (isset($data->refresh_token) && is_string($data->refresh_token)) { + update_option('helloasso_refresh_token', $data->refresh_token); + } + if (isset($data->expires_in) && is_numeric($data->expires_in)) { + update_option('helloasso_token_expires_in', time() + (int) $data->expires_in); + } update_option('helloasso_refresh_token_expires_in', time() + HELLOASSO_REFRESH_TOKEN_LIFETIME); helloasso_log_info('Token OAuth2 rafraîchi avec succès', array( 'new_token_preview' => substr($data->access_token, 0, 10) . '...', - 'expires_in' => $data->expires_in + 'expires_in' => isset($data->expires_in) && is_numeric($data->expires_in) ? (int) $data->expires_in : null )); return $data->access_token; @@ -105,6 +112,8 @@ function helloasso_get_oauth_token($client_id, $client_secret, $api_url) $response_body = wp_remote_retrieve_body($response); $response_code = wp_remote_retrieve_response_code($response); + + /** @var object{access_token?: string, refresh_token?: string, expires_in?: int|string} $data */ $data = json_decode($response_body); helloasso_log_info('Réponse demande token OAuth2', array( @@ -112,15 +121,19 @@ function helloasso_get_oauth_token($client_id, $client_secret, $api_url) 'has_access_token' => isset($data->access_token) )); - if (isset($data->access_token)) { + if (isset($data->access_token) && is_string($data->access_token)) { add_option('helloasso_access_token', $data->access_token); - add_option('helloasso_refresh_token', $data->refresh_token); - add_option('helloasso_token_expires_in', time() + $data->expires_in); + if (isset($data->refresh_token) && is_string($data->refresh_token)) { + add_option('helloasso_refresh_token', $data->refresh_token); + } + if (isset($data->expires_in) && is_numeric($data->expires_in)) { + add_option('helloasso_token_expires_in', time() + (int) $data->expires_in); + } add_option('helloasso_refresh_token_expires_in', time() + HELLOASSO_REFRESH_TOKEN_LIFETIME); helloasso_log_info('Nouveau token OAuth2 obtenu avec succès', array( 'token_preview' => substr($data->access_token, 0, 10) . '...', - 'expires_in' => $data->expires_in + 'expires_in' => isset($data->expires_in) && is_numeric($data->expires_in) ? (int) $data->expires_in : null )); return $data->access_token; @@ -135,4 +148,4 @@ function helloasso_get_oauth_token($client_id, $client_secret, $api_url) helloasso_log_warning('Aucun token OAuth2 disponible et aucun refresh token valide'); return null; -} +} \ No newline at end of file diff --git a/helloasso-woocommerce-gateway.php b/helloasso-woocommerce-gateway.php index 09ecaaa..4b179b2 100644 --- a/helloasso-woocommerce-gateway.php +++ b/helloasso-woocommerce-gateway.php @@ -3,8 +3,9 @@ /** * Plugin Name: HelloAsso Payments for WooCommerce * Description: Recevez 100% de vos paiements gratuitement. HelloAsso est la seule solution de paiement gratuite du secteur associatif. Nous sommes financés librement par la solidarité de celles et ceux qui choisissent de laisser une contribution volontaire au moment du paiement à une association. - * Version: 1.1.2 - * Requires at least: 5.0 + * Version: 1.1.3 + * Requires at least: 6.0 + * Tested up to: 7.0 * WC requires at least: 7.7 * Requires PHP: 7.2.34 * Requires Plugins: woocommerce @@ -23,10 +24,14 @@ * This action hook registers our PHP class as a WooCommerce payment gateway */ require_once plugin_dir_path( __FILE__ ) . 'vendor/autoload.php'; - +// Durée de validité du refresh token : 30 jours en secondes +define('HELLOASSO_REFRESH_TOKEN_LIFETIME', 30 * 24 * 60 * 60); // 2592000 secondes +define('HELLOASSO_PLUGIN_VERSION', '1.1.3'); +define('HELLOASSO_PLUGIN_DIR', plugin_dir_url( __FILE__)); use Automattic\WooCommerce\Utilities\FeaturesUtil; use Automattic\WooCommerce\Blocks\Payments\PaymentMethodRegistry; use Helloasso\HelloassoPaymentsForWoocommerce\Gateway\WC_HelloAsso_Gateway; +use Helloasso\HelloassoPaymentsForWoocommerce\Block\Helloasso_Blocks; require_once('helper/helloasso-woocommerce-api-call.php'); require_once('helper/helloasso-woocommerce-config.php'); @@ -100,6 +105,10 @@ function helloasso_activate() deactivate_plugins(plugin_basename(__FILE__)); wp_die('HelloAsso ne prend en charge que les paiements en euros. Veuillez changer la devise de votre boutique en euros pour activer ce plugin.'); } + helloasso_log_info('Initialisation du gateway HelloAsso', array( + 'plugin_version' => HELLOASSO_PLUGIN_VERSION, + 'wc_version' => defined('WC_VERSION') ? WC_VERSION : 'unknown' + )); } @@ -124,5 +133,4 @@ function helloasso_deactivate() delete_option('helloasso_webhook_data'); } -add_action('wp_ajax_helloasso_deco', 'helloasso_deco'); diff --git a/helper/helloasso-woocommerce-api-call.php b/helper/helloasso-woocommerce-api-call.php index 6ae9d3f..d17313e 100644 --- a/helper/helloasso-woocommerce-api-call.php +++ b/helper/helloasso-woocommerce-api-call.php @@ -2,8 +2,18 @@ if (! defined('ABSPATH')) { exit; //Exit if accessed directly } + +function helloasso_get_user_agent(): string +{ + $woocommerce_db_version = get_option('woocommerce_db_version'); + $woocommerce_db_version = is_string($woocommerce_db_version) ? $woocommerce_db_version : ''; + + return 'PHP ' . PHP_VERSION . '/WooCommerce ' . $woocommerce_db_version; +} + function helloasso_get_args_post_urlencode($data) { + helloasso_log_debug('Préparation requête POST URL-encoded', array( 'data_keys' => array_keys($data), 'data_count' => count($data) @@ -20,7 +30,7 @@ function helloasso_get_args_post_urlencode($data) ), 'body' => http_build_query($data), 'cookies' => array(), - 'user-agent' => 'PHP ' . PHP_VERSION . '/WooCommerce ' . get_option('woocommerce_db_version'), + 'user-agent' => helloasso_get_user_agent() ); return $args; @@ -44,7 +54,7 @@ function helloasso_get_args_post($data) ), 'body' => $data, 'cookies' => array(), - 'user-agent' => 'PHP ' . PHP_VERSION . '/WooCommerce ' . get_option('woocommerce_db_version'), + 'user-agent' => helloasso_get_user_agent(), ); return $args; @@ -71,7 +81,7 @@ function helloasso_get_args_post_token($data, $token) ), 'body' => wp_json_encode($data), 'cookies' => array(), - 'user-agent' => 'PHP ' . PHP_VERSION . '/WooCommerce ' . get_option('woocommerce_db_version'), + 'user-agent' => helloasso_get_user_agent(), ); return $args; @@ -97,7 +107,7 @@ function helloasso_get_args_put_token($data, $token) ), 'body' => wp_json_encode($data), 'cookies' => array(), - 'user-agent' => 'PHP ' . PHP_VERSION . '/WooCommerce ' . get_option('woocommerce_db_version'), + 'user-agent' => helloasso_get_user_agent(), ); return $args; @@ -120,7 +130,7 @@ function helloasso_get_args_get_token($token) 'Authorization' => 'Bearer ' . $token, ), 'cookies' => array(), - 'user-agent' => 'PHP ' . PHP_VERSION . '/WooCommerce ' . get_option('woocommerce_db_version'), + 'user-agent' => helloasso_get_user_agent(), ); return $args; diff --git a/helper/helloasso-woocommerce-helper.php b/helper/helloasso-woocommerce-helper.php index 1a2043a..1d0b520 100644 --- a/helper/helloasso-woocommerce-helper.php +++ b/helper/helloasso-woocommerce-helper.php @@ -14,7 +14,8 @@ function helloasso_log($message, $level = 'info', $context = array()) $context['timestamp'] = current_time('Y-m-d H:i:s'); $context['memory_usage'] = memory_get_usage(true); $context['peak_memory'] = memory_get_peak_usage(true); - + $context['plugin_version'] = HELLOASSO_PLUGIN_VERSION; + $context['wc_version'] = defined('WC_VERSION') ? WC_VERSION : 'unknown'; $log_message = sprintf( '[%s] %s - %s', strtoupper($level), diff --git a/inc/Gateway/WC_HelloAsso_Gateway.php b/inc/Gateway/WC_HelloAsso_Gateway.php index 02e48de..98d6603 100644 --- a/inc/Gateway/WC_HelloAsso_Gateway.php +++ b/inc/Gateway/WC_HelloAsso_Gateway.php @@ -10,17 +10,49 @@ class WC_HelloAsso_Gateway extends \WC_Payment_Gateway */ public $isConnected; + /** + * @var string + */ + public $icon = ''; + /** + * @var string + */ + public $title = ''; + /** + * @var string + */ + public $description = ''; + + /** + * @var string + */ + public $method_description = ''; + + /** + * @var string + */ + public $enabled = ''; + + /** + * @var bool + */ + public $has_fields = false; + + /** @var string[] */ + public $supports = []; + + /** @var array */ + + public $form_fields = []; + public function __construct() { - helloasso_log_info('Initialisation du gateway HelloAsso', array( - 'plugin_version' => '1.1.2', - 'wc_version' => defined('WC_VERSION') ? WC_VERSION : 'unknown' - )); + $this->id = 'helloasso'; - $this->icon = null; + $this->icon = ''; $this->has_fields = true; $this->method_title = 'Payer par carte bancaire avec HelloAsso'; $this->method_description = 'Acceptez des paiements gratuitement avec HelloAsso (0 frais, 0 commission pour votre association).'; @@ -35,14 +67,15 @@ public function __construct() $this->title = $this->get_option('title'); $this->description = 'Le modèle solidaire de HelloAsso garantit que 100% de votre paiement sera versé à l’association choisie. Vous pouvez soutenir l’aide qu’ils apportent aux associations en laissant une contribution volontaire à HelloAsso au moment de votre paiement.'; $this->enabled = $this->get_option('enabled'); - add_action('woocommerce_update_options_payment_gateways_' . $this->id, array($this, 'process_admin_options')); + add_action('woocommerce_update_options_payment_gateways_' . $this->id, array($this, 'save_admin_options')); } public function payment_fields() { if ($this->description) { + echo '
'; - echo 'HelloAsso Logo'; + echo 'HelloAsso Logo'; echo '

' . wp_kses_post($this->description) . '

'; echo '
'; } @@ -80,6 +113,10 @@ public function payment_fields() } } + public function save_admin_options(): void + { + $this->process_admin_options(); + } public function admin_options() { // Check if we have helloasso_access_token_asso in the options @@ -92,7 +129,7 @@ public function admin_options() if (isset($_GET['msg'])) { $msg = sanitize_text_field($_GET['msg']); - if (isset($msg) && 'error_connect' === $msg) { + if ( 'error_connect' === $msg) { if (isset($_GET['status_code']) && '403' === $_GET['status_code']) { echo '

Erreur lors de la connexion à HelloAsso. Veuillez nous contacter. (Erreur 403)

@@ -104,17 +141,22 @@ public function admin_options() } } - if (isset($msg) && 'success_connect' === $msg && $this->isConnected) { + if ( 'success_connect' === $msg && $this->isConnected) { echo '

Connexion à HelloAsso réussie.

'; } if(!$this->isConnected) { - echo '
-

La connexion à HelloAsso est incomplète. Veuillez réessayer.

-
'; - + if(isset($_GET['deconnect']) && 'success' === $_GET['deconnect']) { + echo '
+

Déconnexion de HelloAsso réussie.

+
'; + } else { + echo '
+

La connexion à HelloAsso est incomplète. Veuillez réessayer.

+
'; + } } } @@ -251,8 +293,8 @@ public function admin_options() $("#woocommerce_helloasso_enabled, #woocommerce_helloasso_testmode").change(function() { var enabled = $("#woocommerce_helloasso_enabled").is(":checked") ? 1 : 0; var testmode = $("#woocommerce_helloasso_testmode").is(":checked") ? 1 : 0; - var wasEnabled = ' . esc_js($enabled) . '; - var wasTestMode = ' . esc_js($testmode) . '; + var wasEnabled = ' . wp_json_encode((string) $enabled) . '; + var wasTestMode = ' . wp_json_encode((string) $testmode) . '; var buttonText = "Enregistrer les modifications"; if (enabled == 1 && wasEnabled == 0) { @@ -295,7 +337,9 @@ class="HaAuthorizeButtonLogo"> console.log(data); var data = JSON.parse(data); if (data.success) { - location.reload(); + const url = new URL(window.location.href); + url.searchParams.set("deconnect", "success"); + window.location.replace(url.toString()); } else { alert(data.message); } @@ -376,7 +420,7 @@ public function process_admin_options() // } if ( $this->isConnected && get_option('helloasso_testmode') == $this->get_option('testmode')) { - return; + return true; } delete_option('helloasso_access_token'); @@ -400,7 +444,7 @@ public function process_admin_options() } if ($this->get_option('enabled') !== 'yes') { - return; + return true; } helloasso_get_oauth_token($client_id, $client_secret, $api_url); @@ -426,7 +470,7 @@ public function process_admin_options() exit; } - public function validate_fields() + public function validate_fields(): bool { if (isset($_GET['pay_for_order'])) { return true; @@ -445,10 +489,23 @@ public function validate_fields() $json = file_get_contents('php://input'); $data = json_decode($json, true); + if (!is_array($data)) { + wc_add_notice('Données de commande invalides', 'error'); + return false; + } + + if ( + !isset($data['billing_address']) || + !is_array($data['billing_address']) || + !isset($data['billing_address']['first_name'], $data['billing_address']['last_name'], $data['billing_address']['email']) + ) { + wc_add_notice('Données de facturation incomplètes', 'error'); + return false; + } - $firstName = $data['billing_address']['first_name']; - $lastName = $data['billing_address']['last_name']; - $email = $data['billing_address']['email']; + $firstName = is_string($data['billing_address']['first_name']) ? $data['billing_address']['first_name'] : ''; + $lastName = is_string($data['billing_address']['last_name']) ? $data['billing_address']['last_name'] : ''; + $email = is_string($data['billing_address']['email']) ? $data['billing_address']['email'] : ''; } if (preg_match('/(.)\1{2,}/', $firstName)) { @@ -491,12 +548,12 @@ public function validate_fields() return false; } - if (preg_match('/![a-zA-ZéèêëáàâäúùûüçÇ\'-]/', $firstName)) { + if (preg_match('/[^a-zA-ZéèêëáàâäúùûüçÇ\' -]/', $firstName)) { wc_add_notice('Le prénom ne doit pas contenir de caractères spéciaux ni de caractères n\'appartenant pas à l\'alphabet latin', 'error'); return false; } - if (preg_match('/![a-zA-ZéèêëáàâäúùûüçÇ\'-]/', $lastName)) { + if (preg_match('/[^a-zA-ZéèêëáàâäúùûüçÇ\' -]/', $lastName)) { wc_add_notice('Le nom ne doit pas contenir de caractères spéciaux ni de caractères n\'appartenant pas à l\'alphabet latin', 'error'); return false; } @@ -516,20 +573,30 @@ public function validate_fields() public function process_payment($order_id) { + $order_id = (int) $order_id; + helloasso_log_info('Début du traitement de paiement', array( 'order_id' => $order_id, 'user_id' => get_current_user_id(), 'payment_method' => 'helloasso' )); - helloasso_refresh_token_asso(); + $tokenExpiresRaw = get_option('helloasso_token_expires_in_asso'); + $accessToken = get_option('helloasso_access_token_asso'); + $tokenExpires = is_numeric($tokenExpiresRaw) ? (int) $tokenExpiresRaw : 0; + + if (!$accessToken || !$tokenExpires || time() >= (int) $tokenExpires) { + helloasso_refresh_token_asso(); + } + + $order = wc_get_order($order_id); if (!$order) { helloasso_log_error('Commande introuvable', array('order_id' => $order_id)); return array('result' => 'failure', 'messages' => 'Commande introuvable'); } - + helloasso_log_info('Récupération des données client', array( 'order_id' => $order_id, 'order_status' => $order->get_status(), @@ -617,14 +684,24 @@ public function process_payment($order_id) $json = file_get_contents('php://input'); $data = json_decode($json, true); - $firstName = $data['billing_address']['first_name']; - $lastName = $data['billing_address']['last_name']; - $email = $data['billing_address']['email']; - $adress = $data['billing_address']['address_1']; - $city = $data['billing_address']['city']; - $zipCode = $data['billing_address']['postcode']; - $countryIso = helloasso_convert_country_code($data['billing_address']['country']); - $company = $data['billing_address']['company']; + if (!is_array($data) || !isset($data['billing_address']) || !is_array($data['billing_address'])) { + wc_add_notice('Données de commande invalides', 'error'); + return array( + 'result' => 'failure', + 'messages' => 'Données de commande invalides', + ); + } + + $billingAddress = $data['billing_address']; + + $firstName = isset($billingAddress['first_name']) && is_string($billingAddress['first_name']) ? $billingAddress['first_name'] : ''; + $lastName = isset($billingAddress['last_name']) && is_string($billingAddress['last_name']) ? $billingAddress['last_name'] : ''; + $email = isset($billingAddress['email']) && is_string($billingAddress['email']) ? $billingAddress['email'] : ''; + $adress = isset($billingAddress['address_1']) && is_string($billingAddress['address_1']) ? $billingAddress['address_1'] : ''; + $city = isset($billingAddress['city']) && is_string($billingAddress['city']) ? $billingAddress['city'] : ''; + $zipCode = isset($billingAddress['postcode']) && is_string($billingAddress['postcode']) ? $billingAddress['postcode'] : ''; + $countryIso = isset($billingAddress['country']) && is_string($billingAddress['country']) ? helloasso_convert_country_code($billingAddress['country']) : ''; + $company = isset($billingAddress['company']) && is_string($billingAddress['company']) ? $billingAddress['company'] : ''; helloasso_log_debug('Données client récupérées depuis JSON', array( 'first_name' => $firstName, @@ -692,13 +769,15 @@ public function process_payment($order_id) $payment_type = 'one_time'; if ($this->get_option('multi_3_enabled') === 'yes' || $this->get_option('multi_12_enabled') === 'yes') { - if (isset($_POST['payment_data']) && isset($_POST['payment_data']['payment_type'])) { + $payment_type = 'one_time'; + + if (isset($_POST['payment_data']) && is_array($_POST['payment_data']) && isset($_POST['payment_data']['payment_type']) && is_string($_POST['payment_data']['payment_type'])) { $payment_type = sanitize_text_field($_POST['payment_data']['payment_type']); - } elseif (isset($_POST['payment_type'])) { + } elseif (isset($_POST['payment_type']) && is_string($_POST['payment_type'])) { $payment_type = sanitize_text_field($_POST['payment_type']); - } elseif (isset($_POST['helloasso_payment_type'])) { + } elseif (isset($_POST['helloasso_payment_type']) && is_string($_POST['helloasso_payment_type'])) { $payment_type = sanitize_text_field($_POST['helloasso_payment_type']); - } elseif (isset($_POST['paymentMethodData']) && isset($_POST['paymentMethodData']['payment_type'])) { + } elseif (isset($_POST['paymentMethodData']) && is_array($_POST['paymentMethodData']) && isset($_POST['paymentMethodData']['payment_type']) && is_string($_POST['paymentMethodData']['payment_type'])) { $payment_type = sanitize_text_field($_POST['paymentMethodData']['payment_type']); } @@ -706,12 +785,12 @@ public function process_payment($order_id) $input = file_get_contents('php://input'); $request = json_decode($input, true); - if ($request) { - if (isset($request['payment_data']) && isset($request['payment_data']['payment_type'])) { + if (is_array($request)) { + if (isset($request['payment_data']) && is_array($request['payment_data']) && isset($request['payment_data']['payment_type']) && is_string($request['payment_data']['payment_type'])) { $payment_type = sanitize_text_field($request['payment_data']['payment_type']); - } elseif (isset($request['paymentMethodData']) && isset($request['paymentMethodData']['payment_type'])) { + } elseif (isset($request['paymentMethodData']) && is_array($request['paymentMethodData']) && isset($request['paymentMethodData']['payment_type']) && is_string($request['paymentMethodData']['payment_type'])) { $payment_type = sanitize_text_field($request['paymentMethodData']['payment_type']); - } elseif (isset($request['meta']) && isset($request['meta']['paymentMethodData']) && isset($request['meta']['paymentMethodData']['payment_type'])) { + } elseif (isset($request['meta']) && is_array($request['meta']) && isset($request['meta']['paymentMethodData']) && is_array($request['meta']['paymentMethodData']) && isset($request['meta']['paymentMethodData']['payment_type']) && is_string($request['meta']['paymentMethodData']['payment_type'])) { $payment_type = sanitize_text_field($request['meta']['paymentMethodData']['payment_type']); } else { $payment_type = helloasso_find_payment_type_recursive($request); @@ -811,8 +890,9 @@ public function process_payment($order_id) } else { $api_url = HELLOASSO_WOOCOMMERCE_API_URL_PROD; } - - $url = $api_url . 'v5/organizations/' . get_option('helloasso_organization_slug') . '/checkout-intents'; + $helloasso_organization_slug = get_option('helloasso_organization_slug'); + $helloasso_organization_slug = is_string($helloasso_organization_slug) ? $helloasso_organization_slug : ''; + $url = $api_url . 'v5/organizations/' . $helloasso_organization_slug . '/checkout-intents'; helloasso_log_info('Appel API HelloAsso', array( 'order_id' => $order_id, @@ -828,7 +908,8 @@ public function process_payment($order_id) 'error' => $response->get_error_message(), 'error_code' => $response->get_error_code() )); - echo 'Erreur : ' . esc_html($response->get_error_message()); + wc_add_notice('Une erreur est survenue lors de la connexion à HelloAsso. Veuillez réessayer.', 'error'); + return array('result' => 'failure'); } $response_body = wp_remote_retrieve_body($response); @@ -846,29 +927,32 @@ public function process_payment($order_id) 'response_code' => $response_code, 'response_body' => $response_body )); + wc_add_notice('Une erreur est survenue lors de la création du paiement HelloAsso (code ' . $response_code . '). Veuillez réessayer.', 'error'); + return array('result' => 'failure'); } $response_data = json_decode($response_body); - if (!$response_data || !isset($response_data->redirectUrl)) { + if (!is_object($response_data) || !isset($response_data->redirectUrl) || !is_string($response_data->redirectUrl)) { helloasso_log_error('Réponse API invalide', array( 'order_id' => $order_id, 'response_body' => $response_body, 'response_code' => $response_code )); + wc_add_notice('Réponse inattendue de HelloAsso. Veuillez réessayer.', 'error'); + return array('result' => 'failure'); } helloasso_log_info('Paiement traité avec succès', array( 'order_id' => $order_id, - 'redirect_url' => $response_data->redirectUrl ?? 'unknown' + 'redirect_url' => $response_data->redirectUrl )); - - $order->save(); + $order->save(); return array( 'result' => 'success', - 'redirect' => json_decode($response_body)->redirectUrl + 'redirect' => $response_data->redirectUrl, ); } } diff --git a/phpstan.neon b/phpstan.neon new file mode 100644 index 0000000..55d2bc6 --- /dev/null +++ b/phpstan.neon @@ -0,0 +1,38 @@ +includes: + - vendor/szepeviktor/phpstan-wordpress/extension.neon + +parameters: + level: 5 + scanFiles: + - stubs/woocommerce.php + - stubs/woocommerce-payment-gateway.php + - stubs/woocommerce-payment-gateways.php + - stubs/woocommerce-blocks.php + - stubs/woocommerce-blocks-integrations.php + paths: + - inc + - helloasso-api + - wc-api + - helper + - block + - cron + - helloasso-woocommerce-gateway.php + + excludePaths: + analyse: + - vendor + + + + checkFunctionNameCase: true + checkExplicitMixed: true + tmpDir: var/phpstan + treatPhpDocTypesAsCertain: false + + universalObjectCratesClasses: + - WP_Post + - WP_User + - WC_Product + - WC_Order + - WC_Customer + - WC_Cart \ No newline at end of file diff --git a/readme.txt b/readme.txt index bffda55..a92f5a4 100644 --- a/readme.txt +++ b/readme.txt @@ -3,9 +3,9 @@ Contributors: helloasso Donate link: https://helloasso.com Tags: helloasso, payment, association, don, billetterie Requires at least: 5.0 -Tested up to: 6.9 +Tested up to: 7.0 Requires PHP: 7.2.34 -Stable tag: 1.1.2 +Stable tag: 1.1.3 License: GPLv3 License URI: https://www.gnu.org/licenses/gpl-3.0.html @@ -96,6 +96,14 @@ Please report security bugs found in the source code of the helloasso plugin thr == Changelog == += 1.1.3 = + +* Update logout message on admin panel +* Add phpstan to ensuire compatibility with wordpress and woocommerce +* PHP coverage until 8.5 +* ensure compatibility with woocommerce 10.8.1 +* ensure compatibility with wordpress 7.0 + = 1.1.2 = * Fix contrôle d'expiration des refresh tokens diff --git a/stubs/woocommerce-blocks-integrations.php b/stubs/woocommerce-blocks-integrations.php new file mode 100644 index 0000000..2fa29d6 --- /dev/null +++ b/stubs/woocommerce-blocks-integrations.php @@ -0,0 +1,43 @@ + */ + protected array $settings = []; + + /** @var array */ + protected array $data = []; + + protected $script_version = null; + protected $asset_api = null; + + + + public function is_active() + { + return true; + } + + public function get_payment_method_script_handles() + { + return []; + } + + public function get_payment_method_data() + { + return []; + } + + public function get_name(): string + { + return $this->name; + } + } +} \ No newline at end of file diff --git a/stubs/woocommerce-blocks.php b/stubs/woocommerce-blocks.php new file mode 100644 index 0000000..79b6a67 --- /dev/null +++ b/stubs/woocommerce-blocks.php @@ -0,0 +1,14 @@ + */ + public $settings = []; + + /** + * @var + */ + public $form_fields = []; + + /** @var string */ + public $title = ''; + + /** @var string */ + public $description = ''; + + /** @var string */ + public $enabled = ''; + + /** @var string */ + public $order_button_text = ''; + + /** @var string */ + public $icon = ''; + + public function __construct() + { + } + + /** + * Initialise settings form fields. + * + * @return mixed + */ + public function init_form_fields() + { + } + public function init_settings(): void + { + } + + /** + * @param string $key + * @param mixed $empty_value + * @return mixed + */ + public function get_option($key, $empty_value = null) + { + return $empty_value; + } + + /** + * @param string $key + * @param mixed $value + * @return void + */ + public function update_option($key, $value) + { + } + + /** + * @return bool + */ + public function process_admin_options() + { + return true; + } + + /** + * @param int $order_id + * @return array + */ + public function process_payment($order_id) + { + return []; + } + + public function validate_fields(): bool + { + return true; + } + + /** + * @return mixed + */ + public function payment_fields() + { + } + /** + * @return mixed + */ + public function admin_options() + { + } + + public function get_title(): string + { + return $this->title; + } + + public function get_description(): string + { + return $this->description; + } + + public function get_icon(): string + { + return $this->icon; + } + + public function get_return_url($order = null): string + { + return ''; + } + + public function is_available(): bool + { + return true; + } + + /** + * @return array + */ + public function get_tokens(): array + { + return []; + } + + public function add_error(string $error): void + { + } + + public function get_description_html(): string + { + return $this->description; + } + + public function generate_settings_html(array $form_fields = [], bool $echo = true): string + { + return ''; + } + + } +} \ No newline at end of file diff --git a/stubs/woocommerce-payment-gateways.php b/stubs/woocommerce-payment-gateways.php new file mode 100644 index 0000000..62bba98 --- /dev/null +++ b/stubs/woocommerce-payment-gateways.php @@ -0,0 +1,21 @@ + + */ + public function payment_gateways(): array + { + return []; + } + } +} diff --git a/stubs/woocommerce.php b/stubs/woocommerce.php new file mode 100644 index 0000000..6f98c34 --- /dev/null +++ b/stubs/woocommerce.php @@ -0,0 +1,39 @@ + + * Jordi Boggiano + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Composer; + +use Composer\Autoload\ClassLoader; +use Composer\Semver\VersionParser; + +/** + * This class is copied in every Composer installed project and available to all + * + * See also https://getcomposer.org/doc/07-runtime.md#installed-versions + * + * To require its presence, you can require `composer-runtime-api ^2.0` + * + * @final + */ +class InstalledVersions +{ + /** + * @var mixed[]|null + * @psalm-var array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array}|array{}|null + */ + private static $installed; + + /** + * @var bool|null + */ + private static $canGetVendors; + + /** + * @var array[] + * @psalm-var array}> + */ + private static $installedByVendor = array(); + + /** + * Returns a list of all package names which are present, either by being installed, replaced or provided + * + * @return string[] + * @psalm-return list + */ + public static function getInstalledPackages() + { + $packages = array(); + foreach (self::getInstalled() as $installed) { + $packages[] = array_keys($installed['versions']); + } + + if (1 === \count($packages)) { + return $packages[0]; + } + + return array_keys(array_flip(\call_user_func_array('array_merge', $packages))); + } + + /** + * Returns a list of all package names with a specific type e.g. 'library' + * + * @param string $type + * @return string[] + * @psalm-return list + */ + public static function getInstalledPackagesByType($type) + { + $packagesByType = array(); + + foreach (self::getInstalled() as $installed) { + foreach ($installed['versions'] as $name => $package) { + if (isset($package['type']) && $package['type'] === $type) { + $packagesByType[] = $name; + } + } + } + + return $packagesByType; + } + + /** + * Checks whether the given package is installed + * + * This also returns true if the package name is provided or replaced by another package + * + * @param string $packageName + * @param bool $includeDevRequirements + * @return bool + */ + public static function isInstalled($packageName, $includeDevRequirements = true) + { + foreach (self::getInstalled() as $installed) { + if (isset($installed['versions'][$packageName])) { + return $includeDevRequirements || !isset($installed['versions'][$packageName]['dev_requirement']) || $installed['versions'][$packageName]['dev_requirement'] === false; + } + } + + return false; + } + + /** + * Checks whether the given package satisfies a version constraint + * + * e.g. If you want to know whether version 2.3+ of package foo/bar is installed, you would call: + * + * Composer\InstalledVersions::satisfies(new VersionParser, 'foo/bar', '^2.3') + * + * @param VersionParser $parser Install composer/semver to have access to this class and functionality + * @param string $packageName + * @param string|null $constraint A version constraint to check for, if you pass one you have to make sure composer/semver is required by your package + * @return bool + */ + public static function satisfies(VersionParser $parser, $packageName, $constraint) + { + $constraint = $parser->parseConstraints((string) $constraint); + $provided = $parser->parseConstraints(self::getVersionRanges($packageName)); + + return $provided->matches($constraint); + } + + /** + * Returns a version constraint representing all the range(s) which are installed for a given package + * + * It is easier to use this via isInstalled() with the $constraint argument if you need to check + * whether a given version of a package is installed, and not just whether it exists + * + * @param string $packageName + * @return string Version constraint usable with composer/semver + */ + public static function getVersionRanges($packageName) + { + foreach (self::getInstalled() as $installed) { + if (!isset($installed['versions'][$packageName])) { + continue; + } + + $ranges = array(); + if (isset($installed['versions'][$packageName]['pretty_version'])) { + $ranges[] = $installed['versions'][$packageName]['pretty_version']; + } + if (array_key_exists('aliases', $installed['versions'][$packageName])) { + $ranges = array_merge($ranges, $installed['versions'][$packageName]['aliases']); + } + if (array_key_exists('replaced', $installed['versions'][$packageName])) { + $ranges = array_merge($ranges, $installed['versions'][$packageName]['replaced']); + } + if (array_key_exists('provided', $installed['versions'][$packageName])) { + $ranges = array_merge($ranges, $installed['versions'][$packageName]['provided']); + } + + return implode(' || ', $ranges); + } + + throw new \OutOfBoundsException('Package "' . $packageName . '" is not installed'); + } + + /** + * @param string $packageName + * @return string|null If the package is being replaced or provided but is not really installed, null will be returned as version, use satisfies or getVersionRanges if you need to know if a given version is present + */ + public static function getVersion($packageName) + { + foreach (self::getInstalled() as $installed) { + if (!isset($installed['versions'][$packageName])) { + continue; + } + + if (!isset($installed['versions'][$packageName]['version'])) { + return null; + } + + return $installed['versions'][$packageName]['version']; + } + + throw new \OutOfBoundsException('Package "' . $packageName . '" is not installed'); + } + + /** + * @param string $packageName + * @return string|null If the package is being replaced or provided but is not really installed, null will be returned as version, use satisfies or getVersionRanges if you need to know if a given version is present + */ + public static function getPrettyVersion($packageName) + { + foreach (self::getInstalled() as $installed) { + if (!isset($installed['versions'][$packageName])) { + continue; + } + + if (!isset($installed['versions'][$packageName]['pretty_version'])) { + return null; + } + + return $installed['versions'][$packageName]['pretty_version']; + } + + throw new \OutOfBoundsException('Package "' . $packageName . '" is not installed'); + } + + /** + * @param string $packageName + * @return string|null If the package is being replaced or provided but is not really installed, null will be returned as reference + */ + public static function getReference($packageName) + { + foreach (self::getInstalled() as $installed) { + if (!isset($installed['versions'][$packageName])) { + continue; + } + + if (!isset($installed['versions'][$packageName]['reference'])) { + return null; + } + + return $installed['versions'][$packageName]['reference']; + } + + throw new \OutOfBoundsException('Package "' . $packageName . '" is not installed'); + } + + /** + * @param string $packageName + * @return string|null If the package is being replaced or provided but is not really installed, null will be returned as install path. Packages of type metapackages also have a null install path. + */ + public static function getInstallPath($packageName) + { + foreach (self::getInstalled() as $installed) { + if (!isset($installed['versions'][$packageName])) { + continue; + } + + return isset($installed['versions'][$packageName]['install_path']) ? $installed['versions'][$packageName]['install_path'] : null; + } + + throw new \OutOfBoundsException('Package "' . $packageName . '" is not installed'); + } + + /** + * @return array + * @psalm-return array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool} + */ + public static function getRootPackage() + { + $installed = self::getInstalled(); + + return $installed[0]['root']; + } + + /** + * Returns the raw installed.php data for custom implementations + * + * @deprecated Use getAllRawData() instead which returns all datasets for all autoloaders present in the process. getRawData only returns the first dataset loaded, which may not be what you expect. + * @return array[] + * @psalm-return array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array} + */ + public static function getRawData() + { + @trigger_error('getRawData only returns the first dataset loaded, which may not be what you expect. Use getAllRawData() instead which returns all datasets for all autoloaders present in the process.', E_USER_DEPRECATED); + + if (null === self::$installed) { + // only require the installed.php file if this file is loaded from its dumped location, + // and not from its source location in the composer/composer package, see https://github.com/composer/composer/issues/9937 + if (substr(__DIR__, -8, 1) !== 'C') { + self::$installed = include __DIR__ . '/installed.php'; + } else { + self::$installed = array(); + } + } + + return self::$installed; + } + + /** + * Returns the raw data of all installed.php which are currently loaded for custom implementations + * + * @return array[] + * @psalm-return list}> + */ + public static function getAllRawData() + { + return self::getInstalled(); + } + + /** + * Lets you reload the static array from another file + * + * This is only useful for complex integrations in which a project needs to use + * this class but then also needs to execute another project's autoloader in process, + * and wants to ensure both projects have access to their version of installed.php. + * + * A typical case would be PHPUnit, where it would need to make sure it reads all + * the data it needs from this class, then call reload() with + * `require $CWD/vendor/composer/installed.php` (or similar) as input to make sure + * the project in which it runs can then also use this class safely, without + * interference between PHPUnit's dependencies and the project's dependencies. + * + * @param array[] $data A vendor/composer/installed.php data set + * @return void + * + * @psalm-param array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array} $data + */ + public static function reload($data) + { + self::$installed = $data; + self::$installedByVendor = array(); + } + + /** + * @return array[] + * @psalm-return list}> + */ + private static function getInstalled() + { + if (null === self::$canGetVendors) { + self::$canGetVendors = method_exists('Composer\Autoload\ClassLoader', 'getRegisteredLoaders'); + } + + $installed = array(); + + if (self::$canGetVendors) { + foreach (ClassLoader::getRegisteredLoaders() as $vendorDir => $loader) { + if (isset(self::$installedByVendor[$vendorDir])) { + $installed[] = self::$installedByVendor[$vendorDir]; + } elseif (is_file($vendorDir.'/composer/installed.php')) { + /** @var array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array} $required */ + $required = require $vendorDir.'/composer/installed.php'; + $installed[] = self::$installedByVendor[$vendorDir] = $required; + if (null === self::$installed && strtr($vendorDir.'/composer', '\\', '/') === strtr(__DIR__, '\\', '/')) { + self::$installed = $installed[count($installed) - 1]; + } + } + } + } + + if (null === self::$installed) { + // only require the installed.php file if this file is loaded from its dumped location, + // and not from its source location in the composer/composer package, see https://github.com/composer/composer/issues/9937 + if (substr(__DIR__, -8, 1) !== 'C') { + /** @var array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array} $required */ + $required = require __DIR__ . '/installed.php'; + self::$installed = $required; + } else { + self::$installed = array(); + } + } + + if (self::$installed !== array()) { + $installed[] = self::$installed; + } + + return $installed; + } +} diff --git a/vendor/composer/autoload_files.php b/vendor/composer/autoload_files.php new file mode 100644 index 0000000..b62e293 --- /dev/null +++ b/vendor/composer/autoload_files.php @@ -0,0 +1,10 @@ + $vendorDir . '/phpstan/phpstan/bootstrap.php', +); diff --git a/vendor/composer/autoload_psr4.php b/vendor/composer/autoload_psr4.php index 321b850..4e2edd2 100644 --- a/vendor/composer/autoload_psr4.php +++ b/vendor/composer/autoload_psr4.php @@ -6,5 +6,6 @@ $baseDir = dirname($vendorDir); return array( + 'SzepeViktor\\PHPStan\\WordPress\\' => array($vendorDir . '/szepeviktor/phpstan-wordpress/src'), 'Helloasso\\HelloassoPaymentsForWoocommerce\\' => array($baseDir . '/inc'), ); diff --git a/vendor/composer/autoload_real.php b/vendor/composer/autoload_real.php index 6fac184..6dacb15 100644 --- a/vendor/composer/autoload_real.php +++ b/vendor/composer/autoload_real.php @@ -31,6 +31,18 @@ public static function getLoader() $loader->register(true); + $filesToLoad = \Composer\Autoload\ComposerStaticInit8acb959cdef6e08c0a631c2e2581223e::$files; + $requireFile = \Closure::bind(static function ($fileIdentifier, $file) { + if (empty($GLOBALS['__composer_autoload_files'][$fileIdentifier])) { + $GLOBALS['__composer_autoload_files'][$fileIdentifier] = true; + + require $file; + } + }, null, null); + foreach ($filesToLoad as $fileIdentifier => $file) { + $requireFile($fileIdentifier, $file); + } + return $loader; } } diff --git a/vendor/composer/autoload_static.php b/vendor/composer/autoload_static.php index 350c6f9..993ae7f 100644 --- a/vendor/composer/autoload_static.php +++ b/vendor/composer/autoload_static.php @@ -6,7 +6,15 @@ class ComposerStaticInit8acb959cdef6e08c0a631c2e2581223e { + public static $files = array ( + '9b38cf48e83f5d8f60375221cd213eee' => __DIR__ . '/..' . '/phpstan/phpstan/bootstrap.php', + ); + public static $prefixLengthsPsr4 = array ( + 'S' => + array ( + 'SzepeViktor\\PHPStan\\WordPress\\' => 30, + ), 'H' => array ( 'Helloasso\\HelloassoPaymentsForWoocommerce\\' => 42, @@ -14,6 +22,10 @@ class ComposerStaticInit8acb959cdef6e08c0a631c2e2581223e ); public static $prefixDirsPsr4 = array ( + 'SzepeViktor\\PHPStan\\WordPress\\' => + array ( + 0 => __DIR__ . '/..' . '/szepeviktor/phpstan-wordpress/src', + ), 'Helloasso\\HelloassoPaymentsForWoocommerce\\' => array ( 0 => __DIR__ . '/../..' . '/inc', diff --git a/vendor/composer/installed.json b/vendor/composer/installed.json new file mode 100644 index 0000000..7b73e81 --- /dev/null +++ b/vendor/composer/installed.json @@ -0,0 +1,198 @@ +{ + "packages": [ + { + "name": "php-stubs/wordpress-stubs", + "version": "v6.9.4", + "version_normalized": "6.9.4.0", + "source": { + "type": "git", + "url": "https://github.com/php-stubs/wordpress-stubs.git", + "reference": "90a9412826b9944f93b10bf41d795b5fe68abcd5" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-stubs/wordpress-stubs/zipball/90a9412826b9944f93b10bf41d795b5fe68abcd5", + "reference": "90a9412826b9944f93b10bf41d795b5fe68abcd5", + "shasum": "" + }, + "conflict": { + "phpdocumentor/reflection-docblock": "5.6.1" + }, + "require-dev": { + "dealerdirect/phpcodesniffer-composer-installer": "^1.0", + "nikic/php-parser": "^5.5", + "php": "^7.4 || ^8.0", + "php-stubs/generator": "^0.8.6", + "phpdocumentor/reflection-docblock": "^6.0", + "phpstan/phpstan": "^2.1", + "phpunit/phpunit": "^9.5", + "symfony/polyfill-php80": "*", + "szepeviktor/phpcs-psr-12-neutron-hybrid-ruleset": "^1.1.1", + "wp-coding-standards/wpcs": "3.1.0 as 2.3.0" + }, + "suggest": { + "paragonie/sodium_compat": "Pure PHP implementation of libsodium", + "symfony/polyfill-php80": "Symfony polyfill backporting some PHP 8.0+ features to lower PHP versions", + "szepeviktor/phpstan-wordpress": "WordPress extensions for PHPStan" + }, + "time": "2026-05-01T20:36:01+00:00", + "type": "library", + "installation-source": "dist", + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "WordPress function and class declaration stubs for static analysis.", + "homepage": "https://github.com/php-stubs/wordpress-stubs", + "keywords": [ + "PHPStan", + "static analysis", + "wordpress" + ], + "support": { + "issues": "https://github.com/php-stubs/wordpress-stubs/issues", + "source": "https://github.com/php-stubs/wordpress-stubs/tree/v6.9.4" + }, + "install-path": "../php-stubs/wordpress-stubs" + }, + { + "name": "phpstan/phpstan", + "version": "2.2.1", + "version_normalized": "2.2.1.0", + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phpstan/phpstan/zipball/dea9c8f2d25cc849391042b71e429c1a4bf82660", + "reference": "dea9c8f2d25cc849391042b71e429c1a4bf82660", + "shasum": "" + }, + "require": { + "php": "^7.4|^8.0" + }, + "conflict": { + "phpstan/phpstan-shim": "*" + }, + "time": "2026-05-28T14:44:12+00:00", + "bin": [ + "phpstan", + "phpstan.phar" + ], + "type": "library", + "installation-source": "dist", + "autoload": { + "files": [ + "bootstrap.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Ondřej Mirtes" + }, + { + "name": "Markus Staab" + }, + { + "name": "Vincent Langlet" + } + ], + "description": "PHPStan - PHP Static Analysis Tool", + "keywords": [ + "dev", + "static analysis" + ], + "support": { + "docs": "https://phpstan.org/user-guide/getting-started", + "forum": "https://github.com/phpstan/phpstan/discussions", + "issues": "https://github.com/phpstan/phpstan/issues", + "security": "https://github.com/phpstan/phpstan/security/policy", + "source": "https://github.com/phpstan/phpstan-src" + }, + "funding": [ + { + "url": "https://github.com/ondrejmirtes", + "type": "github" + }, + { + "url": "https://github.com/phpstan", + "type": "github" + } + ], + "install-path": "../phpstan/phpstan" + }, + { + "name": "szepeviktor/phpstan-wordpress", + "version": "v2.0.3", + "version_normalized": "2.0.3.0", + "source": { + "type": "git", + "url": "https://github.com/szepeviktor/phpstan-wordpress.git", + "reference": "aa722f037b2d034828cd6c55ebe9e5c74961927e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/szepeviktor/phpstan-wordpress/zipball/aa722f037b2d034828cd6c55ebe9e5c74961927e", + "reference": "aa722f037b2d034828cd6c55ebe9e5c74961927e", + "shasum": "" + }, + "require": { + "php": "^7.4 || ^8.0", + "php-stubs/wordpress-stubs": "^6.6.2", + "phpstan/phpstan": "^2.0" + }, + "require-dev": { + "composer/composer": "^2.1.14", + "composer/semver": "^3.4", + "dealerdirect/phpcodesniffer-composer-installer": "^1.0", + "php-parallel-lint/php-parallel-lint": "^1.1", + "phpstan/phpstan-strict-rules": "^2.0", + "phpunit/phpunit": "^9.0", + "szepeviktor/phpcs-psr-12-neutron-hybrid-ruleset": "^1.0", + "wp-coding-standards/wpcs": "3.1.0 as 2.3.0" + }, + "suggest": { + "swissspidy/phpstan-no-private": "Detect usage of internal core functions, classes and methods" + }, + "time": "2025-09-14T02:58:22+00:00", + "type": "phpstan-extension", + "extra": { + "phpstan": { + "includes": [ + "extension.neon" + ] + } + }, + "installation-source": "dist", + "autoload": { + "psr-4": { + "SzepeViktor\\PHPStan\\WordPress\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "WordPress extensions for PHPStan", + "keywords": [ + "PHPStan", + "code analyse", + "code analysis", + "static analysis", + "wordpress" + ], + "support": { + "issues": "https://github.com/szepeviktor/phpstan-wordpress/issues", + "source": "https://github.com/szepeviktor/phpstan-wordpress/tree/v2.0.3" + }, + "install-path": "../szepeviktor/phpstan-wordpress" + } + ], + "dev": true, + "dev-package-names": [ + "php-stubs/wordpress-stubs", + "phpstan/phpstan", + "szepeviktor/phpstan-wordpress" + ] +} diff --git a/vendor/composer/installed.php b/vendor/composer/installed.php new file mode 100644 index 0000000..245533e --- /dev/null +++ b/vendor/composer/installed.php @@ -0,0 +1,50 @@ + array( + 'name' => 'helloasso/helloasso-payments-for-woocommerce', + 'pretty_version' => 'dev-main', + 'version' => 'dev-main', + 'reference' => '69ced7a552ef32c9f8c21e7eecaf2202de4ab789', + 'type' => 'library', + 'install_path' => __DIR__ . '/../../', + 'aliases' => array(), + 'dev' => true, + ), + 'versions' => array( + 'helloasso/helloasso-payments-for-woocommerce' => array( + 'pretty_version' => 'dev-main', + 'version' => 'dev-main', + 'reference' => '69ced7a552ef32c9f8c21e7eecaf2202de4ab789', + 'type' => 'library', + 'install_path' => __DIR__ . '/../../', + 'aliases' => array(), + 'dev_requirement' => false, + ), + 'php-stubs/wordpress-stubs' => array( + 'pretty_version' => 'v6.9.4', + 'version' => '6.9.4.0', + 'reference' => '90a9412826b9944f93b10bf41d795b5fe68abcd5', + 'type' => 'library', + 'install_path' => __DIR__ . '/../php-stubs/wordpress-stubs', + 'aliases' => array(), + 'dev_requirement' => true, + ), + 'phpstan/phpstan' => array( + 'pretty_version' => '2.2.1', + 'version' => '2.2.1.0', + 'reference' => 'dea9c8f2d25cc849391042b71e429c1a4bf82660', + 'type' => 'library', + 'install_path' => __DIR__ . '/../phpstan/phpstan', + 'aliases' => array(), + 'dev_requirement' => true, + ), + 'szepeviktor/phpstan-wordpress' => array( + 'pretty_version' => 'v2.0.3', + 'version' => '2.0.3.0', + 'reference' => 'aa722f037b2d034828cd6c55ebe9e5c74961927e', + 'type' => 'phpstan-extension', + 'install_path' => __DIR__ . '/../szepeviktor/phpstan-wordpress', + 'aliases' => array(), + 'dev_requirement' => true, + ), + ), +); diff --git a/wc-api/helloasso-woocommerce-wc-api.php b/wc-api/helloasso-woocommerce-wc-api.php index 19d8955..be2c1a7 100644 --- a/wc-api/helloasso-woocommerce-wc-api.php +++ b/wc-api/helloasso-woocommerce-wc-api.php @@ -3,8 +3,7 @@ exit; //Exit if accessed directly } -// Durée de validité du refresh token : 30 jours en secondes -define('HELLOASSO_REFRESH_TOKEN_LIFETIME', 30 * 24 * 60 * 60); // 2592000 secondes + add_action('woocommerce_api_helloasso', 'helloasso_endpoint'); function helloasso_endpoint() @@ -84,8 +83,10 @@ function helloasso_endpoint() $response_body = wp_remote_retrieve_body($response); $data = json_decode($response_body); - - if (isset($data->access_token)) { + if (!is_object($data)) { + return null; + } + if (isset($data->access_token) && is_string($data->access_token)) { helloasso_log_info('Token OAuth2 reçu avec succès', array( 'organization_slug' => $data->organization_slug ?? 'unknown', 'expires_in' => $data->expires_in ?? 'unknown' @@ -122,7 +123,10 @@ function helloasso_endpoint() 'response_body' => wp_remote_retrieve_body($responseNotif) )); - $gateway_settings = get_option('woocommerce_helloasso_settings', array()); + $gateway_settings = is_array(get_option('woocommerce_helloasso_settings', array())) + ? get_option('woocommerce_helloasso_settings', array()) + : array(); + $gateway_settings['enabled'] = 'no'; update_option('woocommerce_helloasso_settings', $gateway_settings); @@ -157,6 +161,11 @@ function helloasso_endpoint() function helloasso_endpoint_deco() { $gateway_settings = get_option('woocommerce_helloasso_settings', array()); + + if (!is_array($gateway_settings)) { + $gateway_settings = array(); + } + $gateway_settings['enabled'] = 'no'; $gateway_settings['multi_3_enabled'] = 'no'; $gateway_settings['multi_12_enabled'] = 'no'; @@ -187,30 +196,59 @@ function helloasso_endpoint_webhook() $raw_input = file_get_contents('php://input'); $data = json_decode($raw_input, true); + if (!is_array($data)) { + helloasso_log_error('Payload webhook invalide', array( + 'raw_data_length' => strlen($raw_input), + )); + exit; + } + + /** @var array{ + * eventType?: string, + * metadata?: array{reference?: string}, + * data?: array{checkoutIntentId?: string, new_slug_organization?: string} + * } $data + */ + helloasso_log_info('Webhook HelloAsso reçu', array( - 'event_type' => $data['eventType'] ?? 'unknown', + 'event_type' => isset($data['eventType']) && is_string($data['eventType']) ? $data['eventType'] : 'unknown', 'raw_data_length' => strlen($raw_input) )); add_option('helloasso_webhook_data', wp_json_encode($data)); - if ('Order' === $data['eventType']) { + if (($data['eventType'] ?? null) === 'Order') { + $metadata = isset($data['metadata']) && is_array($data['metadata']) ? $data['metadata'] : array(); + $payload = isset($data['data']) && is_array($data['data']) ? $data['data'] : array(); + + $reference = isset($metadata['reference']) && is_string($metadata['reference']) ? $metadata['reference'] : 'unknown'; + $checkoutIntentId = isset($payload['checkoutIntentId']) && is_string($payload['checkoutIntentId']) ? $payload['checkoutIntentId'] : 'unknown'; + helloasso_log_info('Traitement d\'un événement Order', array( - 'order_reference' => $data['metadata']['reference'] ?? 'unknown', - 'checkout_intent_id' => $data['data']['checkoutIntentId'] ?? 'unknown' + 'order_reference' => $reference, + 'checkout_intent_id' => $checkoutIntentId )); - validate_order($data['metadata']['reference'], $data['data']['checkoutIntentId']); - } else if ('Organization' === $data['eventType']) { + + if ($reference !== 'unknown' && $checkoutIntentId !== 'unknown') { + validate_order($reference, $checkoutIntentId); + } + } elseif (($data['eventType'] ?? null) === 'Organization') { + $payload = isset($data['data']) && is_array($data['data']) ? $data['data'] : array(); + $newSlug = isset($payload['new_slug_organization']) && is_string($payload['new_slug_organization']) ? $payload['new_slug_organization'] : 'unknown'; + helloasso_log_info('Traitement d\'un événement Organization', array( - 'new_slug' => $data['data']['new_slug_organization'] ?? 'unknown' + 'new_slug' => $newSlug )); + delete_option('helloasso_organization_slug'); - add_option('helloasso_organization_slug', $data['data']['new_slug_organization']); + if ($newSlug !== 'unknown') { + add_option('helloasso_organization_slug', $newSlug); + } helloasso_refresh_token_asso(); } else { helloasso_log_warning('Événement webhook non reconnu', array( - 'event_type' => $data['eventType'] ?? 'unknown' + 'event_type' => isset($data['eventType']) && is_string($data['eventType']) ? $data['eventType'] : 'unknown' )); } @@ -273,8 +311,10 @@ function validate_order($orderId, $checkoutIntentId) 'has_token' => !empty($helloasso_access_token_asso) )); - $url = $api_url . 'v5/organizations/' . $slug . '/checkout-intents/' . $checkoutIntentId; - $response = wp_remote_request($url, helloasso_get_args_get_token($helloasso_access_token_asso)); + $slug = is_string($slug) ? $slug : ''; + $checkoutIntentId = is_string($checkoutIntentId) ? $checkoutIntentId : ''; + + $url = $api_url . 'v5/organizations/' . $slug . '/checkout-intents/' . $checkoutIntentId; $response = wp_remote_request($url, helloasso_get_args_get_token($helloasso_access_token_asso)); $response_code = wp_remote_retrieve_response_code($response); $body = wp_remote_retrieve_body($response); @@ -296,7 +336,14 @@ function validate_order($orderId, $checkoutIntentId) $haOrder = json_decode($body); - if (!$haOrder || !isset($haOrder->order) || !isset($haOrder->order->payments) || empty($haOrder->order->payments)) { + if (!is_object($haOrder) || !isset($haOrder->order) || !is_object($haOrder->order)) { + helloasso_log_error('Réponse JSON invalide', array( + 'order_id' => $orderId, + 'response_body' => $body, + )); + return $order; + } + if ( !isset($haOrder->order) || !isset($haOrder->order->payments) || empty($haOrder->order->payments)) { helloasso_log_error('Structure de réponse HelloAsso invalide', array( 'order_id' => $orderId, 'response_body' => $body