diff --git a/.github/workflows/php.yml b/.github/workflows/php.yml index f01522f..4c853b5 100644 --- a/.github/workflows/php.yml +++ b/.github/workflows/php.yml @@ -90,24 +90,24 @@ jobs: - name: Verify release package layout run: | mkdir -p release - rsync -av --exclude-from=.distignore . release/hellotext-wordpress/ + rsync -av --exclude-from=.distignore . release/hellotext/ - test -f release/hellotext-wordpress/hellotext.php - test -d release/hellotext-wordpress/src - test -f release/hellotext-wordpress/vendor/autoload.php - test -f release/hellotext-wordpress/assets/hellotext-service-worker.js - test -f release/hellotext-wordpress/assets/storefront.js + test -f release/hellotext/hellotext.php + test -d release/hellotext/src + test -f release/hellotext/vendor/autoload.php + test -f release/hellotext/assets/hellotext-service-worker.js + test -f release/hellotext/assets/storefront.js for path in \ - release/hellotext-wordpress/tests \ - release/hellotext-wordpress/composer.json \ - release/hellotext-wordpress/composer.lock \ - release/hellotext-wordpress/.github \ - release/hellotext-wordpress/.php-cs-fixer.cache \ - release/hellotext-wordpress/.php-cs-fixer.php \ - release/hellotext-wordpress/DEVELOPMENT.md \ - release/hellotext-wordpress/API.md \ - release/hellotext-wordpress/release; do + release/hellotext/tests \ + release/hellotext/composer.json \ + release/hellotext/composer.lock \ + release/hellotext/.github \ + release/hellotext/.php-cs-fixer.cache \ + release/hellotext/.php-cs-fixer.php \ + release/hellotext/DEVELOPMENT.md \ + release/hellotext/API.md \ + release/hellotext/release; do if [ -e "$path" ]; then echo "Unexpected development file in release package: $path" exit 1 diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 31af36d..f1fdcfd 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -24,12 +24,12 @@ jobs: - name: Create release zip run: | mkdir -p release - rsync -av --exclude-from=.distignore . release/hellotext-wordpress/ + rsync -av --exclude-from=.distignore . release/hellotext/ cd release - zip -r hellotext-wordpress.zip hellotext-wordpress + zip -r hellotext.zip hellotext - name: Create GitHub Release uses: softprops/action-gh-release@efb35369e0ad2afab669f228072c1b0d510eae64 # v3.0.3 with: - files: release/hellotext-wordpress.zip + files: release/hellotext.zip generate_release_notes: true diff --git a/changelog.txt b/changelog.txt index 5628def..7be4d93 100644 --- a/changelog.txt +++ b/changelog.txt @@ -1,5 +1,13 @@ ** Hellotext for WooCommerce Changelog ** +2026-11-07 - version 2.0.2 + +* Run on PHP 7.4 again: 2.0.0 failed to load there. +* Declare the supported WordPress, WooCommerce and PHP versions, and compatibility with High-Performance Order Storage. +* Log failed Hellotext requests to the WooCommerce logs (source "hellotext") when WP_DEBUG is on. +* Sanitize the checkout link's products and the Webchat card's source before reading them. +* The release zip is hellotext.zip and installs to the hellotext folder, as the Woo Marketplace requires. + 2026-11-06 - version 2.0.1 * Declare WooCommerce as a required plugin: WordPress 6.5 and later only activate Hellotext while WooCommerce is active. diff --git a/hellotext.php b/hellotext.php index 2cabed5..2070c6e 100644 --- a/hellotext.php +++ b/hellotext.php @@ -8,8 +8,13 @@ * Plugin Name: Hellotext * Plugin URI: https://github.com/hellotext/hellotext-wordpress * Description: Integrates Hellotext tracking to WooCommerce. - * Version: 2.0.1 + * Version: 2.0.2 + * Requires at least: 6.5 + * Tested up to: 7.1 + * Requires PHP: 7.4 * Requires Plugins: woocommerce + * WC requires at least: 8.0 + * WC tested up to: 11.2 * Author: Hellotext * Author URI: https://www.hellotext.com * License: GPL v2 @@ -39,6 +44,19 @@ register_activation_hook(__FILE__, 'hellotext_activate'); register_deactivation_hook(__FILE__, 'hellotext_deactivate'); +/** + * Declare compatibility with High-Performance Order Storage: orders are only + * read and written through the WooCommerce order API. + * + * @return void + */ +function hellotext_declare_compatibility() { + if (class_exists(\Automattic\WooCommerce\Utilities\FeaturesUtil::class)) { + \Automattic\WooCommerce\Utilities\FeaturesUtil::declare_compatibility('custom_order_tables', __FILE__, true); + } +} +add_action('before_woocommerce_init', 'hellotext_declare_compatibility'); + /** * Check for plugin updates from GitHub. * Caches result for 24 hours to avoid rate limiting. diff --git a/readme.txt b/readme.txt index 219cdb5..7d18ccb 100644 --- a/readme.txt +++ b/readme.txt @@ -1,6 +1,9 @@ === Hellotext === Contributors: ricardohellotext -Stable tag: 2.0.1 +Requires at least: 6.5 +Tested up to: 7.1 +Requires PHP: 7.4 +Stable tag: 2.0.2 License: GPLv2 License URI: https://www.gnu.org/licenses/gpl-2.0.html diff --git a/src/Adapters/OrderAdapter.php b/src/Adapters/OrderAdapter.php index 8a1fad0..55f5995 100644 --- a/src/Adapters/OrderAdapter.php +++ b/src/Adapters/OrderAdapter.php @@ -15,7 +15,7 @@ class OrderAdapter { * * @var \WC_Order|false */ - public \WC_Order|false $order; + public $order; /** * Adapted order items. @@ -30,7 +30,7 @@ class OrderAdapter { * @param int|\WC_Order $order Order ID or order instance. * @param array $products Pre-adapted products list. */ - public function __construct(int|\WC_Order $order, array $products = []) { + public function __construct($order, array $products = []) { $this->order = is_numeric($order) ? wc_get_order($order) : $order; $this->products = $products; } diff --git a/src/Adapters/PriceAdapter.php b/src/Adapters/PriceAdapter.php index 30b2bfb..5eedfaf 100644 --- a/src/Adapters/PriceAdapter.php +++ b/src/Adapters/PriceAdapter.php @@ -15,7 +15,7 @@ class PriceAdapter { * * @var float|string */ - public float|string $price; + public $price; /** * Currency code. @@ -30,7 +30,7 @@ class PriceAdapter { * @param float|string $price Raw price amount. * @param string|null $currency Currency code or null to use store default. */ - public function __construct(float|string $price, ?string $currency = null) { + public function __construct($price, ?string $currency = null) { $this->price = $price; $this->currency = is_null($currency) ? get_woocommerce_currency() : $currency; } diff --git a/src/Adapters/ProductAdapter.php b/src/Adapters/ProductAdapter.php index 6bcbf4f..56d167c 100644 --- a/src/Adapters/ProductAdapter.php +++ b/src/Adapters/ProductAdapter.php @@ -15,14 +15,14 @@ class ProductAdapter { * * @var \WC_Product|false */ - public \WC_Product|false $product; + public $product; /** * Create a new adapter instance. * * @param int|\WC_Product $product Product ID or product instance. */ - public function __construct(int|\WC_Product $product) { + public function __construct($product) { $this->product = is_numeric($product) ? wc_get_product($product) : $product; } diff --git a/src/Api/Client.php b/src/Api/Client.php index d73005b..7554199 100644 --- a/src/Api/Client.php +++ b/src/Api/Client.php @@ -45,12 +45,12 @@ public static function request(string $method = 'GET', string $path = '/', array // Handle errors if (is_wp_error($response)) { if (defined('WP_DEBUG') && WP_DEBUG) { - error_log(sprintf( - '[Hellotext] API request failed: %s %s - Error: %s', + wc_get_logger()->error(sprintf( + 'API request failed: %s %s - Error: %s', $method, $request_url, $response->get_error_message() - )); + ), ['source' => 'hellotext']); } return [ @@ -70,12 +70,12 @@ public static function request(string $method = 'GET', string $path = '/', array // Log non-2xx responses if (defined('WP_DEBUG') && WP_DEBUG && ($status_code < 200 || $status_code >= 300)) { - error_log(sprintf( - '[Hellotext] API request returned %d: %s %s', + wc_get_logger()->error(sprintf( + 'API request returned %d: %s %s', $status_code, $method, $request_url - )); + ), ['source' => 'hellotext']); } return [ diff --git a/src/Api/Event.php b/src/Api/Event.php index 578b478..440533e 100644 --- a/src/Api/Event.php +++ b/src/Api/Event.php @@ -76,11 +76,11 @@ public function track(string $action, array $payload): void { // Only log if there's an error and debug is enabled if (is_wp_error($response) && defined('WP_DEBUG') && WP_DEBUG) { - error_log(sprintf( - '[Hellotext] Event tracking failed for action "%s": %s', + wc_get_logger()->error(sprintf( + 'Event tracking failed for action "%s": %s', $action, $response->get_error_message() - )); + ), ['source' => 'hellotext']); } } } diff --git a/src/Events/AppInstalled.php b/src/Events/AppInstalled.php index 87ececc..3a417e9 100644 --- a/src/Events/AppInstalled.php +++ b/src/Events/AppInstalled.php @@ -86,7 +86,7 @@ function hellotext_connect(): void { * @param mixed $value Access token being saved. * @return mixed */ -function hellotext_retry_failed_connection(mixed $value): mixed { +function hellotext_retry_failed_connection($value) { if (get_option(Constants::OPTION_CONNECTION_FAILED)) { hellotext_schedule_connect(); } @@ -139,7 +139,7 @@ function hellotext_schedule_connect(): void { * @param mixed $new_value New value. * @return void */ -function hellotext_business_id_updated(mixed $old_value, mixed $new_value): void { +function hellotext_business_id_updated($old_value, $new_value): void { if ($old_value && $old_value !== $new_value) { hellotext_disconnect((string) $old_value); } diff --git a/src/Events/CartUpdates.php b/src/Events/CartUpdates.php index 31dcf79..7228cac 100644 --- a/src/Events/CartUpdates.php +++ b/src/Events/CartUpdates.php @@ -181,7 +181,7 @@ function hellotext_cart_event_source(): ?array { return null; } - $source = json_decode(wp_unslash($_POST['hellotext_source']), true); + $source = json_decode(sanitize_text_field(wp_unslash($_POST['hellotext_source'])), true); if (!is_array($source)) { return null; diff --git a/src/Events/CheckoutLink.php b/src/Events/CheckoutLink.php index 2d84e71..822002c 100644 --- a/src/Events/CheckoutLink.php +++ b/src/Events/CheckoutLink.php @@ -32,8 +32,8 @@ function hellotext_checkout_link(): void { * cart, with WooCommerce's notices, when nothing could be added. */ function hellotext_fill_checkout_link_cart(): string { - $products = wp_unslash($_GET['products'] ?? ''); - $lines = is_string($products) ? hellotext_checkout_link_lines($products) : []; + $products = sanitize_text_field(wp_unslash($_GET['products'] ?? '')); + $lines = hellotext_checkout_link_lines($products); if (empty($lines)) { return wc_get_cart_url(); diff --git a/src/Events/CustomerProfile.php b/src/Events/CustomerProfile.php index db4f958..ba12781 100644 --- a/src/Events/CustomerProfile.php +++ b/src/Events/CustomerProfile.php @@ -11,7 +11,7 @@ * @param mixed $customer Guest billing details, a user ID, or null for the current user. * @return void */ -add_action('hellotext_create_profile', function (mixed $customer = null): void { +add_action('hellotext_create_profile', function ($customer = null): void { if (is_array($customer)) { (new CreateProfile(null, $customer))->process(); return; diff --git a/src/Events/OrderPlaced.php b/src/Events/OrderPlaced.php index f5a62a7..98c5963 100644 --- a/src/Events/OrderPlaced.php +++ b/src/Events/OrderPlaced.php @@ -56,7 +56,7 @@ function hellotext_capture_checkout(\WC_Order $order): void { * @param mixed $order_id Order ID. * @return void */ -function hellotext_thankyou(mixed $order_id): void { +function hellotext_thankyou($order_id): void { $order = wc_get_order($order_id); if (!$order) { diff --git a/src/Events/OrderStatus.php b/src/Events/OrderStatus.php index 50297aa..ffde887 100644 --- a/src/Events/OrderStatus.php +++ b/src/Events/OrderStatus.php @@ -24,12 +24,12 @@ function hellotext_order_status_changed(int $order_id, string $old_status, strin hellotext_order_placed($order); } - $action = match ($new_status) { + $actions = [ 'processing' => Constants::EVENT_ORDER_CONFIRMED, 'cancelled' => Constants::EVENT_ORDER_CANCELLED, 'completed' => Constants::EVENT_ORDER_DELIVERED, - default => null, - }; + ]; + $action = $actions[$new_status] ?? null; if (!$action) { return; diff --git a/src/Services/CreateProfile.php b/src/Services/CreateProfile.php index 4693162..68da15d 100644 --- a/src/Services/CreateProfile.php +++ b/src/Services/CreateProfile.php @@ -119,12 +119,12 @@ public function create_hellotext_profile(): ?string { private function attribute(string $name): ?string { if ($this->user_id) { $user = get_user_by('id', $this->user_id); - $value = match ($name) { + $values = [ 'first_name' => $user->first_name ?? '', 'last_name' => $user->last_name ?? '', 'email' => $user->user_email ?? '', - default => '', - }; + ]; + $value = $values[$name] ?? ''; if ('' === $value) { $value = (string) get_user_meta($this->user_id, "billing_{$name}", true); diff --git a/src/Services/Session.php b/src/Services/Session.php index 8c61b5a..c8e808c 100644 --- a/src/Services/Session.php +++ b/src/Services/Session.php @@ -67,7 +67,7 @@ public static function encrypt(?string $session = null): string { * @param mixed $encrypted_data Encrypted session data. * @return string|false */ - public static function decrypt(mixed $encrypted_data = null): string|false { + public static function decrypt($encrypted_data = null) { if (!is_string($encrypted_data) || '' === $encrypted_data) { return false; }