diff --git a/.vscode/settings.json b/.vscode/settings.json index fd7d3e1c..eef7ecd7 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -75,7 +75,7 @@ "editor.defaultFormatter": "esbenp.prettier-vscode" }, "[javascriptreact]": { - "editor.defaultFormatter": "esbenp.prettier-vscode" + "editor.defaultFormatter": "vscode.typescript-language-features" }, "typescript.validate.enable": false } diff --git a/backend/Actions/Bento/BentoController.php b/backend/Actions/Bento/BentoController.php index d192235f..5fd99b32 100644 --- a/backend/Actions/Bento/BentoController.php +++ b/backend/Actions/Bento/BentoController.php @@ -7,8 +7,8 @@ namespace BitApps\Integrations\Actions\Bento; use BitApps\Integrations\Config; -use BitApps\Integrations\Core\Util\HttpHelper; use BitApps\Integrations\Core\Util\Hooks; +use BitApps\Integrations\Core\Util\HttpHelper; use WP_Error; /** @@ -44,21 +44,25 @@ public function getAllFields($fieldsRequestParams) $defaultFields = [(object) ['label' => __('Email Address', 'bit-integrations'), 'key' => 'email', 'required' => true]]; $fields = Hooks::apply(Config::withPrefix('bento_get_user_fields'), $defaultFields, $fieldsRequestParams); - /** - * @deprecated 2.7.8 Use `bit_integrations_bento_get_user_fields` filter instead. - * @since 2.7.8 - */ - $fields = Hooks::apply('btcbi_bento_get_user_fields', $fields, $fieldsRequestParams); + if (empty($fields)) { + /** + * @deprecated 2.7.8 Use `bit_integrations_bento_get_user_fields` filter instead. + * @since 2.7.8 + */ + $fields = Hooks::apply('btcbi_bento_get_user_fields', $defaultFields, $fieldsRequestParams); + } break; case 'add_event': $fields = Hooks::apply(Config::withPrefix('bento_get_event_fields'), []); - /** - * @deprecated 2.7.8 Use `bit_integrations_bento_get_event_fields` filter instead. - * @since 2.7.8 - */ - $fields = Hooks::apply('btcbi_bento_get_event_fields', $fields); + if (empty($fields)) { + /** + * @deprecated 2.7.8 Use `bit_integrations_bento_get_event_fields` filter instead. + * @since 2.7.8 + */ + $fields = Hooks::apply('btcbi_bento_get_event_fields', []); + } break; @@ -77,11 +81,13 @@ public function getAlTags($fieldsRequestParams) $tags = Hooks::apply(Config::withPrefix('bento_get_all_tags'), [], $fieldsRequestParams); - /** - * @deprecated 2.7.8 Use `bit_integrations_bento_get_all_tags` filter instead. - * @since 2.7.8 - */ - $tags = Hooks::apply('btcbi_bento_get_all_tags', $tags, $fieldsRequestParams); + if (empty($tags)) { + /** + * @deprecated 2.7.8 Use `bit_integrations_bento_get_all_tags` filter instead. + * @since 2.7.8 + */ + $tags = Hooks::apply('btcbi_bento_get_all_tags', [], $fieldsRequestParams); + } wp_send_json_success($tags, 200); } diff --git a/backend/Actions/CustomAction/CustomActionController.php b/backend/Actions/CustomAction/CustomActionController.php index 00d85f76..acc7310b 100644 --- a/backend/Actions/CustomAction/CustomActionController.php +++ b/backend/Actions/CustomAction/CustomActionController.php @@ -2,7 +2,7 @@ namespace BitApps\Integrations\Actions\CustomAction; -use BitApps\Integrations\Core\Util\PhpSyntaxChecker; +use BitApps\Integrations\Core\Util\CustomFuncValidator; use BitApps\Integrations\Log\LogHandler; use Throwable; @@ -10,19 +10,17 @@ class CustomActionController { public static function functionValidateHandler($data) { - $result = PhpSyntaxChecker::validate($data); - - if ($result['is_valid']) { - wp_send_json_success( - sprintf( - /* translators: %s: validation result message */ - __('Congrats, %s', 'bit-integrations'), - $result['message'] - ) - ); + if (empty($data)) { + wp_send_json_error(__('No function content provided.', 'bit-integrations')); + + return; + } + + if (!CustomFuncValidator::loopbackValidateContent($data)) { + return; } - wp_send_json_error($result['message']); + wp_send_json_success(__('No syntax errors detected in your function.', 'bit-integrations')); } public function execute($integrationData, $fieldValues) diff --git a/backend/Actions/Dokan/RecordApiHelper.php b/backend/Actions/Dokan/RecordApiHelper.php index e6d76990..948e868f 100644 --- a/backend/Actions/Dokan/RecordApiHelper.php +++ b/backend/Actions/Dokan/RecordApiHelper.php @@ -83,11 +83,13 @@ public function formatVendorUpsertData($finalData, $actions, $module) $filterResponse = Hooks::apply(Config::withPrefix('dokan_vendor_crud_actions'), $module, $actions); - /** - * @deprecated 2.7.8 Use `bit_integrations_dokan_vendor_crud_actions` filter instead. - * @since 2.7.8 - */ - $filterResponse = Hooks::apply('btcbi_dokan_vendor_crud_actions', $filterResponse, $module, $actions); + if (empty($filterResponse)) { + /** + * @deprecated 2.7.8 Use `bit_integrations_dokan_vendor_crud_actions` filter instead. + * @since 2.7.8 + */ + $filterResponse = Hooks::apply('btcbi_dokan_vendor_crud_actions', $module, $actions); + } if ($filterResponse !== $module && !empty($filterResponse)) { $data = array_merge($data, $filterResponse); diff --git a/backend/Actions/FluentCrm/RecordApiHelper.php b/backend/Actions/FluentCrm/RecordApiHelper.php index a411ccd4..49982eb7 100644 --- a/backend/Actions/FluentCrm/RecordApiHelper.php +++ b/backend/Actions/FluentCrm/RecordApiHelper.php @@ -138,11 +138,13 @@ public function execute($fieldValues, $fieldMap, $actions, $list_id, $tags, $act { $fieldData = Hooks::apply(Config::withPrefix('fluent_crm_assign_company'), [], (array) $actions); - /** - * @deprecated 2.7.8 Use `bit_integrations_fluent_crm_assign_company` filter instead. - * @since 2.7.8 - */ - $fieldData = Hooks::apply('btcbi_fluent_crm_assign_company', $fieldData, (array) $actions); + if (empty($fieldData)) { + /** + * @deprecated 2.7.8 Use `bit_integrations_fluent_crm_assign_company` filter instead. + * @since 2.7.8 + */ + $fieldData = Hooks::apply('btcbi_fluent_crm_assign_company', [], (array) $actions); + } foreach ($fieldMap as $fieldKey => $fieldPair) { if (!empty($fieldPair->fluentCRMField)) { diff --git a/backend/Actions/Trello/TrelloController.php b/backend/Actions/Trello/TrelloController.php index 0e18f243..2d90f7c9 100644 --- a/backend/Actions/Trello/TrelloController.php +++ b/backend/Actions/Trello/TrelloController.php @@ -7,8 +7,8 @@ namespace BitApps\Integrations\Actions\Trello; use BitApps\Integrations\Config; -use BitApps\Integrations\Core\Util\HttpHelper; use BitApps\Integrations\Core\Util\Hooks; +use BitApps\Integrations\Core\Util\HttpHelper; use WP_Error; /** @@ -118,12 +118,13 @@ public function fetchAllCustomFields($queryParams) $allFields = Hooks::apply(Config::withPrefix('trello_get_all_custom_fields'), [], $queryParams->boardId, $queryParams->clientId, $queryParams->accessToken); - /** - * @deprecated 2.7.8 Use `bit_integrations_trello_get_all_custom_fields` filter instead. - * @since 2.7.8 - */ - $allFields = Hooks::apply('btcbi_trello_get_all_custom_fields', $allFields, $queryParams->boardId, $queryParams->clientId, $queryParams->accessToken); - + if (empty($allFields)) { + /** + * @deprecated 2.7.8 Use `bit_integrations_trello_get_all_custom_fields` filter instead. + * @since 2.7.8 + */ + $allFields = Hooks::apply('btcbi_trello_get_all_custom_fields', [], $queryParams->boardId, $queryParams->clientId, $queryParams->accessToken); + } wp_send_json_success($allFields, 200); } diff --git a/backend/Actions/WooCommerce/WooCommerceMetaFields.php b/backend/Actions/WooCommerce/WooCommerceMetaFields.php index 15d6e247..22b2e1e9 100644 --- a/backend/Actions/WooCommerce/WooCommerceMetaFields.php +++ b/backend/Actions/WooCommerce/WooCommerceMetaFields.php @@ -146,11 +146,13 @@ private static function getFlexibleCheckoutFields() $checkoutFields = []; $fields = Hooks::apply(Config::withPrefix('woocommerce_flexible_checkout_fields'), []); - /** - * @deprecated 2.7.8 Use `bit_integrations_woocommerce_flexible_checkout_fields` filter instead. - * @since 2.7.8 - */ - $fields = Hooks::apply('btcbi_woocommerce_flexible_checkout_fields', $fields); + if (empty($fields)) { + /** + * @deprecated 2.7.8 Use `bit_integrations_woocommerce_flexible_checkout_fields` filter instead. + * @since 2.7.8 + */ + $fields = Hooks::apply('btcbi_woocommerce_flexible_checkout_fields', []); + } foreach ($fields as $field) { $checkoutFields[$field->fieldName] = (object) [ diff --git a/backend/Actions/ZohoBigin/ZohoBiginController.php b/backend/Actions/ZohoBigin/ZohoBiginController.php index bbbe0236..88033501 100644 --- a/backend/Actions/ZohoBigin/ZohoBiginController.php +++ b/backend/Actions/ZohoBigin/ZohoBiginController.php @@ -357,11 +357,13 @@ public static function getTagList($queryParams) $accessToken = isset($response['tokenDetails']->access_token) ? $response['tokenDetails']->access_token : $queryParams->tokenDetails->access_token; $response['tags'] = Hooks::apply(Config::withPrefix('zbigin_get_tags'), [], $accessToken, $queryParams->dataCenter, $queryParams->module); - /** - * @deprecated 2.7.8 Use `bit_integrations_zbigin_get_tags` filter instead. - * @since 2.7.8 - */ - $response['tags'] = Hooks::apply('btcbi_zbigin_get_tags', $response['tags'], $accessToken, $queryParams->dataCenter, $queryParams->module); + if (empty($response['tags'])) { + /** + * @deprecated 2.7.8 Use `bit_integrations_zbigin_get_tags` filter instead. + * @since 2.7.8 + */ + $response['tags'] = Hooks::apply('btcbi_zbigin_get_tags', [], $accessToken, $queryParams->dataCenter, $queryParams->module); + } if (!empty($response['tokenDetails']) && !empty($queryParams->id)) { self::saveRefreshedToken($queryParams->id, $response['tokenDetails'], $response['lists']); diff --git a/backend/Config.php b/backend/Config.php index 5dd9b027..7f346163 100644 --- a/backend/Config.php +++ b/backend/Config.php @@ -22,7 +22,7 @@ class Config public const VAR_PREFIX = 'bit_integrations_'; - public const VERSION = '2.7.10'; + public const VERSION = '2.7.11'; public const DB_VERSION = '1.1'; diff --git a/backend/Core/Util/CustomFuncValidator.php b/backend/Core/Util/CustomFuncValidator.php index 6436f506..f83b28a7 100644 --- a/backend/Core/Util/CustomFuncValidator.php +++ b/backend/Core/Util/CustomFuncValidator.php @@ -2,36 +2,311 @@ namespace BitApps\Integrations\Core\Util; +use WP_Filesystem_Base; +use BitApps\Integrations\Config; + class CustomFuncValidator { public static function functionValidateHandler($data) { - $fileContent = html_entity_decode($data->flow_details->value, ENT_QUOTES, 'UTF-8'); + if (empty($data->flow_details->value)) { + wp_send_json_error(__('No function content provided.', 'bit-integrations')); + + return; + } + + if (empty($data->flow_details->randomFileName)) { + wp_send_json_error(__('No file name provided.', 'bit-integrations')); + + return; + } + + $fileContent = $data->flow_details->value; $fileName = $data->flow_details->randomFileName; - $checkingValue = "defined('ABSPATH')"; - $isExits = str_contains($fileContent, $checkingValue); - $checkFuncIsValid = self::functionIsValid($fileContent); - - if ($isExits && $checkFuncIsValid) { - global $wp_filesystem; - - if (empty($wp_filesystem)) { - require_once ABSPATH . '/wp-admin/includes/file.php'; - WP_Filesystem(); + + if (strpos($fileContent, "defined('ABSPATH')") === false) { + wp_send_json_error(__("Your function must include a defined('ABSPATH') check.", 'bit-integrations')); + + return; + } + + $fileWriteResult = self::writeCustomFunctionFile($fileName, $fileContent); + if (false === $fileWriteResult) { + return; + } + + if (!self::loopbackCheck($fileWriteResult['fileLocation'], $fileWriteResult['previousContent'], $fileWriteResult['filesystem'])) { + return; + } + + $data->flow_details->funcFileLocation = $fileWriteResult['fileLocation']; + } + + /** + * Handles the loopback scrape request for a custom action file. + * Registers its own shutdown function to output result markers so we are + * not dependent on WP's wp_start_scraping_edited_file_errors(), which only + * runs during full admin page loads and never for admin-ajax.php requests. + * + * @param object $data Sanitized GET params (bit_integrations_scrape_key). + */ + public static function scrapeCustomActionFile($data) + { + if (empty($data->bit_integrations_scrape_key)) { + wp_die(0); + } + + $scrapeKey = sanitize_key($data->bit_integrations_scrape_key); + $fileLocation = get_transient(Config::withPrefix('scrape_file_') . $scrapeKey); + + if (false === $fileLocation || !file_exists($fileLocation)) { + wp_die(0); + } + + $needleStart = '###### ' . Config::withPrefix("result_start:{$scrapeKey}") . ' ######'; + $needleEnd = '###### ' . Config::withPrefix("result_end:{$scrapeKey}") . ' ######'; + + $fatalTypes = [E_ERROR, E_PARSE, E_CORE_ERROR, E_COMPILE_ERROR, E_USER_ERROR, E_RECOVERABLE_ERROR]; + + register_shutdown_function(function () use ($needleStart, $needleEnd, $fatalTypes) { + $error = error_get_last(); + + echo "\n{$needleStart}\n"; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped + + if (!empty($error) && \in_array($error['type'], $fatalTypes, true)) { + // Take the first line only — PHP 8 uncaught errors include a multi-line + // stack trace with absolute file paths on every line. + $message = strtok($error['message'], "\n"); + + // Strip the trailing " in /path/file.php on line N" (PHP 7/8 parse/fatal) + // or " in /path/file.php:N" (PHP 8 uncaught Error short format). + $message = (string) preg_replace('/ in .+\.php(:\d+| on line \d+)$/', '', (string) $message); + + echo wp_json_encode([ // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped + 'type' => 'php_error', + 'message' => trim($message), + 'line' => $error['line'], + ]); + } else { + echo wp_json_encode(true); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped + } + + echo "\n{$needleEnd}\n"; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped + }); + + // phpcs:ignore WordPressVIPMinimum.Files.IncludingFile.UsingVariable + include $fileLocation; + + wp_die(); + } + + /** + * Validates PHP code via the loopback check without saving a permanent file. + * Writes a temporary file, runs the loopback, then deletes the temp file. + * Calls wp_send_json_error() and returns false on any PHP fatal; returns true on success. + * + * @param string $fileContent PHP code to validate. + * + * @return bool + */ + public static function loopbackValidateContent($fileContent) + { + $wp_filesystem = self::getFilesystem(); + if (false === $wp_filesystem) { + wp_send_json_error(__('Unable to initialize filesystem.', 'bit-integrations')); + + return false; + } + + $uploadDir = wp_upload_dir(); + $tmpFile = "{$uploadDir['basedir']}/" . Config::withPrefix('tmp_') . md5(wp_rand()) . '.php'; + + $written = $wp_filesystem->put_contents($tmpFile, $fileContent, FS_CHMOD_FILE); + + if (!$written) { + wp_send_json_error(__('Unable to write temporary file for validation.', 'bit-integrations')); + + return false; + } + + // previousContent is null so loopbackCheck deletes the temp file on failure. + $passed = self::loopbackCheck($tmpFile, null, $wp_filesystem); + + if ($passed) { + $wp_filesystem->delete($tmpFile); + } + + return $passed; + } + + /** + * Get initialized WP filesystem instance. + * + * @return WP_Filesystem_Base|false + */ + private static function getFilesystem() + { + global $wp_filesystem; + + if (empty($wp_filesystem)) { + require_once ABSPATH . '/wp-admin/includes/file.php'; + + if (!WP_Filesystem()) { + return false; } + } + + return $wp_filesystem instanceof WP_Filesystem_Base ? $wp_filesystem : false; + } + + /** + * Initialize filesystem, resolve file path, and write custom function content. + * + * @param string $fileName + * @param string $fileContent + * + * @return array{filesystem: WP_Filesystem_Base, fileLocation: string, previousContent: string|null}|false + */ + private static function writeCustomFunctionFile($fileName, $fileContent) + { + $wp_filesystem = self::getFilesystem(); + if (false === $wp_filesystem) { + wp_send_json_error(__('Unable to initialize filesystem.', 'bit-integrations')); + + return false; + } + + $uploadDir = wp_upload_dir(); + $fileLocation = "{$uploadDir['basedir']}/{$fileName}.php"; + $previousContent = file_exists($fileLocation) ? file_get_contents($fileLocation) : null; + $written = $wp_filesystem->put_contents($fileLocation, $fileContent, FS_CHMOD_FILE); + + if (!$written) { + wp_send_json_error(__('Unable to write to file.', 'bit-integrations')); + + return false; + } + + return [ + 'filesystem' => $wp_filesystem, + 'fileLocation' => $fileLocation, + 'previousContent' => $previousContent, + ]; + } + + /** + * Mirrors wp_edit_theme_plugin_file()'s loopback check. + * Sets up wp_scrape_key/wp_scrape_nonce transients so WP's built-in + * wp_start_scraping_edited_file_errors() registers the shutdown handler, + * makes a loopback request to include the written file, parses the result + * markers, and rolls back the file on any PHP fatal. + * + * @param string $fileLocation Absolute path to the written file. + * @param string|null $previousContent Previous file content for rollback, or null if new. + * @param WP_Filesystem_Base $wp_filesystem + * + * @return bool True on success, false if a fatal was detected (error already sent). + */ + private static function loopbackCheck($fileLocation, $previousContent, $wp_filesystem) + { + $scrapeKey = md5(wp_rand()); + + set_transient(Config::withPrefix('scrape_file_') . $scrapeKey, $fileLocation, 60); + + $cookies = wp_unslash($_COOKIE); // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized + + $scrapeParams = [ + 'action' => Config::withPrefix('custom-action/scrape'), + Config::withPrefix('scrape_key') => $scrapeKey, + ]; + + $headers = ['Cache-Control' => 'no-cache']; + + /** This filter is documented in wp-includes/class-wp-http-streams.php */ + $sslverify = apply_filters('https_local_ssl_verify', false); // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound - $uploadDir = wp_upload_dir(); - $fileLocation = "{$uploadDir['basedir']}/{$fileName}.php"; - $data->flow_details->funcFileLocation = $fileLocation; - $wp_filesystem->put_contents($fileLocation, $fileContent, FS_CHMOD_FILE); + if (isset($_SERVER['PHP_AUTH_USER'], $_SERVER['PHP_AUTH_PW'])) { + $headers['Authorization'] = 'Basic ' . base64_encode( // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_encode + wp_unslash($_SERVER['PHP_AUTH_USER']) . ':' . wp_unslash($_SERVER['PHP_AUTH_PW']) // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized + ); + } + + if (\function_exists('set_time_limit')) { + set_time_limit(5 * MINUTE_IN_SECONDS); // phpcs:ignore Squiz.PHP.DiscouragedFunctions.Discouraged + } + + if (\function_exists('session_status') && PHP_SESSION_ACTIVE === session_status()) { + session_write_close(); + } + + $timeout = 100; + $url = add_query_arg($scrapeParams, admin_url('admin-ajax.php')); + + $response = wp_remote_get($url, compact('cookies', 'headers', 'timeout', 'sslverify')); + $body = wp_remote_retrieve_body($response); + + $needleStart = '###### ' . Config::withPrefix("result_start:{$scrapeKey}") . ' ######'; + $needleEnd = '###### ' . Config::withPrefix("result_end:{$scrapeKey}") . ' ######'; + + $loopbackFailure = [ + 'code' => 'loopback_request_failed', + 'message' => __('Unable to communicate back with site to check for fatal errors, so the PHP change was reverted. You will need to fix any issues manually.', 'bit-integrations'), + ]; + + $resultPos = strpos($body, $needleStart); + + if (false === $resultPos) { + $result = $loopbackFailure; } else { - wp_send_json_error('Your function is not valid, Failed to save file'); + $errorOutput = substr($body, $resultPos + \strlen($needleStart)); + $errorOutput = substr($errorOutput, 0, strpos($errorOutput, $needleEnd)); + $result = json_decode(trim($errorOutput), true); + + if (empty($result)) { + $result = ['code' => 'json_parse_error']; + } + } + + delete_transient(Config::withPrefix('scrape_file_') . $scrapeKey); + + if (true !== $result) { + if ($previousContent !== null) { + $wp_filesystem->put_contents($fileLocation, $previousContent, FS_CHMOD_FILE); + } else { + $wp_filesystem->delete($fileLocation); + } + + wp_send_json_error(self::formatLoopbackError($result)); + + return false; } + + return true; } - public static function functionIsValid($fileContent) + /** + * Converts a raw loopback scrape result into a safe, user-readable message. + * Strips server file paths from PHP error strings before sending to the frontend. + * + * @param array $result Decoded scrape result. + * + * @return string + */ + private static function formatLoopbackError($result) { - $result = PhpSyntaxChecker::validate($fileContent); - return $result['is_valid']; + if (isset($result['code']) && $result['code'] === 'loopback_request_failed') { + return $result['message']; + } + + if (isset($result['type']) && $result['type'] === 'php_error' && isset($result['message'], $result['line'])) { + return sprintf( + /* translators: 1: line number, 2: PHP error message */ + __('PHP error on line %1$d: %2$s', 'bit-integrations'), + (int) $result['line'], + $result['message'] + ); + } + + return __('An error occurred while verifying the function. Please try again.', 'bit-integrations'); } } diff --git a/backend/Core/Util/Helper.php b/backend/Core/Util/Helper.php index 4df36edf..2b7ec884 100644 --- a/backend/Core/Util/Helper.php +++ b/backend/Core/Util/Helper.php @@ -323,15 +323,25 @@ public static function getWCCustomCheckoutData($order) $isOrderObject = \is_object($order) && method_exists($order, 'get_meta'); $orderData = \is_object($order) && method_exists($order, 'get_data') ? $order->get_data() : (array) $order; - foreach ($checkoutFields as $group) { - foreach ($group as $field) { - if (empty($field['custom'])) { - continue; - } - - $fieldName = $field['name']; + foreach ($checkoutFields as $groupKey => $group) { + foreach ($group as $fieldKey => $field) { + $fieldName = $field['name'] ?? $fieldKey; + $metaKey = null; + + // if field matches booster for woocommerce plugin custom key format + if (strpos($fieldKey, "{$groupKey}_wcj_checkout_field_") !== false) { + if ($isOrderObject) { + if ($order->meta_exists($fieldName)) { + $metaKey = $fieldName; + } elseif ($order->meta_exists("_{$fieldName}")) { + $metaKey = "_{$fieldName}"; + } - if ($isOrderObject && $order->meta_exists($fieldName)) { + $data[$fieldName] = $metaKey ? $order->get_meta($metaKey, true) : ''; + } else { + $data[$fieldName] = $orderData[$fieldName] ?? ''; + } + } elseif ($isOrderObject && $order->meta_exists($fieldName)) { $data[$fieldName] = $order->get_meta($fieldName, true) ?? ''; } else { $data[$fieldName] = $orderData[$fieldName] ?? ''; diff --git a/backend/Core/Util/PhpSyntaxChecker.php b/backend/Core/Util/PhpSyntaxChecker.php deleted file mode 100644 index 52767072..00000000 --- a/backend/Core/Util/PhpSyntaxChecker.php +++ /dev/null @@ -1,47 +0,0 @@ - false, - 'message' => __('Empty code provided.', 'bit-integrations'), - ]; - } - - try { - if (\defined('TOKEN_PARSE')) { - token_get_all($code, TOKEN_PARSE); - } else { - token_get_all($code); - } - - return [ - 'is_valid' => true, - 'message' => __('No syntax errors detected in your function.', 'bit-integrations'), - ]; - } catch (\ParseError $e) { - return [ - 'is_valid' => false, - 'message' => $e->getMessage(), - ]; - } - } - - -} diff --git a/backend/Core/Util/Route.php b/backend/Core/Util/Route.php index 99611f02..fef5a723 100644 --- a/backend/Core/Util/Route.php +++ b/backend/Core/Util/Route.php @@ -13,6 +13,8 @@ final class Route private static $_ignore_token = false; + private static $_no_sanitize = false; + public static function get($hook, $invokeable) { return static::request('GET', $hook, $invokeable); @@ -43,6 +45,10 @@ public static function request($method, $hook, $invokeable) static::$_ignore_token = false; } + if (static::$_no_sanitize) { + static::$_no_sanitize = false; + } + return; } @@ -51,6 +57,11 @@ public static function request($method, $hook, $invokeable) static::$_invokeable[Config::VAR_PREFIX . $hook][$method . '_ignore_token'] = true; } + if (static::$_no_sanitize) { + static::$_no_sanitize = false; + static::$_invokeable[Config::VAR_PREFIX . $hook][$method . '_no_sanitize'] = true; + } + static::$_invokeable[Config::VAR_PREFIX . $hook][$method] = $invokeable; Hooks::add('wp_ajax_' . Config::VAR_PREFIX . $hook, [__CLASS__, 'action']); @@ -88,6 +99,9 @@ public static function action() unset($_POST['_ajax_nonce'], $_POST['action'], $_GET['_ajax_nonce'], $_GET['action']); if (method_exists($invokeable[0], $invokeable[1])) { + $noSanitize = isset(static::$_invokeable[$action][$requestMethod . '_no_sanitize']) + && static::$_invokeable[$action][$requestMethod . '_no_sanitize']; + if ($requestMethod == 'POST') { if ( isset($_SERVER['CONTENT_TYPE']) @@ -96,16 +110,24 @@ public static function action() ) { $inputJSON = file_get_contents('php://input'); $decoded = \is_string($inputJSON) ? json_decode($inputJSON) : $inputJSON; - $data = \is_object($decoded) || \is_array($decoded) ? map_deep($decoded, 'sanitize_text_field') : $decoded; + $data = $noSanitize + ? $decoded + : (\is_object($decoded) || \is_array($decoded) ? map_deep($decoded, 'sanitize_text_field') : $decoded); } elseif (isset($_POST['data'])) { // phpcs:ignore WordPress.Security.NonceVerification.Missing $postReq = wp_unslash($_POST['data']); // phpcs:ignore WordPress.Security.NonceVerification.Missing, WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Sanitized via map_deep on next lines $decoded = \is_string($postReq) ? json_decode($postReq) : $postReq; - $data = \is_object($decoded) || \is_array($decoded) ? map_deep($decoded, 'sanitize_text_field') : $decoded; + $data = $noSanitize + ? $decoded + : (\is_object($decoded) || \is_array($decoded) ? map_deep($decoded, 'sanitize_text_field') : $decoded); } else { - $data = (object) map_deep(wp_unslash($_POST), 'sanitize_text_field'); // phpcs:ignore WordPress.Security.NonceVerification.Missing + $data = $noSanitize // phpcs:ignore WordPress.Security.NonceVerification.Missing + ? (object) wp_unslash($_POST) // phpcs:ignore WordPress.Security.NonceVerification.Missing, WordPress.Security.ValidatedSanitizedInput.InputNotSanitized + : (object) map_deep(wp_unslash($_POST), 'sanitize_text_field'); // phpcs:ignore WordPress.Security.NonceVerification.Missing } } else { - $data = (object) map_deep(wp_unslash($_GET), 'sanitize_text_field'); // phpcs:ignore WordPress.Security.NonceVerification.Recommended + $data = $noSanitize // phpcs:ignore WordPress.Security.NonceVerification.Recommended + ? (object) wp_unslash($_GET) // phpcs:ignore WordPress.Security.NonceVerification.Recommended, WordPress.Security.ValidatedSanitizedInput.InputNotSanitized + : (object) map_deep(wp_unslash($_GET), 'sanitize_text_field'); // phpcs:ignore WordPress.Security.NonceVerification.Recommended } $reflectionMethod = new ReflectionMethod($invokeable[0], $invokeable[1]); @@ -144,6 +166,13 @@ public static function ignore_token() return new static(); } + public static function no_sanitize() + { + self::$_no_sanitize = true; + + return new static(); + } + private static function getActionFromRequest() { if (isset($_REQUEST['action'])) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended diff --git a/backend/Routes/ajax.php b/backend/Routes/ajax.php index 49247b87..846ea696 100644 --- a/backend/Routes/ajax.php +++ b/backend/Routes/ajax.php @@ -5,10 +5,11 @@ exit; } -use BitApps\Integrations\controller\AuthDataController; use BitApps\Integrations\controller\AnalyticsController; +use BitApps\Integrations\controller\AuthDataController; use BitApps\Integrations\controller\PostController; use BitApps\Integrations\controller\UserController; +use BitApps\Integrations\Core\Util\CustomFuncValidator; use BitApps\Integrations\Core\Util\Route; use BitApps\Integrations\Flow\Flow; use BitApps\Integrations\Log\LogHandler; @@ -32,6 +33,11 @@ Route::post('flow/toggleStatus', [Flow::class, 'toggle_status']); Route::post('flow/clone', [Flow::class, 'flowClone']); +// Custom Action +Route::no_sanitize()->post('flow/custom-action/save', [Flow::class, 'save']); +Route::no_sanitize()->post('flow/custom-action/update', [Flow::class, 'update']); +Route::no_auth()->ignore_token()->get('custom-action/scrape', [CustomFuncValidator::class, 'scrapeCustomActionFile']); + // Controller Route::post('customfield/list', [PostController::class, 'getCustomFields']); Route::get('pods/list', [PostController::class, 'getPodsPostType']); diff --git a/backend/Triggers/WC/WCHelper.php b/backend/Triggers/WC/WCHelper.php index f2eeaed0..48559979 100644 --- a/backend/Triggers/WC/WCHelper.php +++ b/backend/Triggers/WC/WCHelper.php @@ -164,11 +164,13 @@ public static function processOrderData($orderId, $order = null, $extra = []) $flexibleFields = Hooks::apply(Config::withPrefix('woocommerce_flexible_checkout_fields_value'), (array) $order); - /** - * @deprecated 2.7.8 Use `bit_integrations_woocommerce_flexible_checkout_fields_value` filter instead. - * @since 2.7.8 - */ - $flexibleFields = Hooks::apply('btcbi_woocommerce_flexible_checkout_fields_value', $flexibleFields); + if (empty($flexibleFields)) { + /** + * @deprecated 2.7.8 Use `bit_integrations_woocommerce_flexible_checkout_fields_value` filter instead. + * @since 2.7.8 + */ + $flexibleFields = Hooks::apply('btcbi_woocommerce_flexible_checkout_fields_value', (array) $order); + } return array_merge($orderData, $acfFielddata, $checkoutFields, $flexibleFields, $extra); } diff --git a/backend/Triggers/WC/WCStaticFields.php b/backend/Triggers/WC/WCStaticFields.php index accb0ecd..fcba42fc 100644 --- a/backend/Triggers/WC/WCStaticFields.php +++ b/backend/Triggers/WC/WCStaticFields.php @@ -282,8 +282,19 @@ private static function getCheckoutCustomFields() $fields = []; $checkoutFields = WC()->checkout()->get_checkout_fields(); - foreach ($checkoutFields as $group) { - foreach ($group as $field) { + foreach ($checkoutFields as $groupKey => $group) { + foreach ($group as $fieldKey => $field) { + if (strpos($fieldKey, "{$groupKey}_wcj_checkout_field_") !== false) { + $fieldName = $field['name'] ?? $fieldKey; + + $fields[$fieldName] = (object) [ + 'fieldKey' => $fieldName, + 'fieldName' => $field['label'] + ]; + + continue; + } + if (!empty($field['custom']) && $field['custom']) { $fields[$field['name']] = (object) [ 'fieldKey' => $field['name'], @@ -300,11 +311,13 @@ private static function getFlexibleCheckoutFields() { $fields = Hooks::apply(Config::withPrefix('woocommerce_flexible_checkout_fields'), []); - /** - * @deprecated 2.7.8 Use `bit_integrations_woocommerce_flexible_checkout_fields` filter instead. - * @since 2.7.8 - */ - $fields = Hooks::apply('btcbi_woocommerce_flexible_checkout_fields', $fields); + if (empty($fields)) { + /** + * @deprecated 2.7.8 Use `bit_integrations_woocommerce_flexible_checkout_fields` filter instead. + * @since 2.7.8 + */ + $fields = Hooks::apply('btcbi_woocommerce_flexible_checkout_fields', []); + } return $fields; } diff --git a/bitwpfi.php b/bitwpfi.php index a58ed39c..7059354a 100644 --- a/bitwpfi.php +++ b/bitwpfi.php @@ -4,7 +4,7 @@ * Plugin Name: Bit Integrations * Plugin URI: https://bitapps.pro/bit-integrations * Description: Bit Integrations is a platform that integrates with over 300+ different platforms to help with various tasks on your WordPress site, like WooCommerce, Form builder, Page builder, LMS, Sales funnels, Bookings, CRM, Webhooks, Email marketing, Social media and Spreadsheets, etc - * Version: 2.7.10 + * Version: 2.7.11 * Author: Automation & Integration Plugin - Bit Apps * Author URI: https://bitapps.pro * Text Domain: bit-integrations @@ -33,7 +33,7 @@ * * @deprecated 2.7.8 Use Config::VERSION instead. */ -define('BTCBI_VERSION', '2.7.10'); +define('BTCBI_VERSION', '2.7.11'); /** * deprecated since version 2.7.8. * diff --git a/frontend/src/components/AllIntegrations/IntegrationHelpers/IntegrationHelpers.js b/frontend/src/components/AllIntegrations/IntegrationHelpers/IntegrationHelpers.js index 1ad04fd6..aec0abc1 100644 --- a/frontend/src/components/AllIntegrations/IntegrationHelpers/IntegrationHelpers.js +++ b/frontend/src/components/AllIntegrations/IntegrationHelpers/IntegrationHelpers.js @@ -223,6 +223,11 @@ export const saveIntegConfig = async ( if (edit) { action = 'flow/update' } + + if (confTmp?.type === 'CustomAction') { + action = edit ? 'flow/custom-action/update' : 'flow/custom-action/save' + } + try { const res = await bitsFetch(data, action) if (!edit && res.success) { @@ -405,6 +410,11 @@ export const saveActionConf = async ({ if (edit) { action = 'flow/update' } + + if (conf?.type === 'CustomAction') { + action = edit ? 'flow/custom-action/update' : 'flow/custom-action/save' + } + try { await bitsFetch(data, action).then(res => { if (!edit && res.success) { diff --git a/frontend/src/components/Triggers/Webhook.jsx b/frontend/src/components/Triggers/Webhook.jsx index 2cfd90e5..597b76a4 100644 --- a/frontend/src/components/Triggers/Webhook.jsx +++ b/frontend/src/components/Triggers/Webhook.jsx @@ -15,8 +15,6 @@ import EyeOffIcn from '../Utilities/EyeOffIcn' import Note from '../Utilities/Note' import { APP_CONFIG } from '../../config/app' -console.log('APP_CONFIG : ', APP_CONFIG) - const Webhook = () => { const [newFlow, setNewFlow] = useRecoilState($newFlow) const setFlowStep = useSetRecoilState($flowStep) diff --git a/frontend/src/pages/ChangelogToggle.jsx b/frontend/src/pages/ChangelogToggle.jsx index 025d0f6c..fd0bb710 100644 --- a/frontend/src/pages/ChangelogToggle.jsx +++ b/frontend/src/pages/ChangelogToggle.jsx @@ -9,7 +9,7 @@ import NewYear from '../resource/img/NewYear.png' import bitsFetch from '../Utils/bitsFetch' import { __, sprintf } from '../Utils/i18nwrap' -const releaseDate = '20th February 2026' +const releaseDate = '26th February 2026' // Example for items: // items: [ @@ -42,13 +42,30 @@ const changeLog = [ label: __('New Features', 'bit-integrations'), headClass: 'new-feature', itemClass: 'feature-list', - items: [] + items: [ + { + label: 'WooCommerce', + desc: 'Booster for WooCommerce checkout fields added.', + isPro: false + } + ] }, { label: __('Improvements', 'bit-integrations'), headClass: 'new-improvement', itemClass: 'feature-list', - items: [] + items: [ + { + label: 'Breakdance', + desc: 'Updated Breakdance trigger test endpoints to use the shared trigger test routes.', + isPro: true + }, + { + label: 'Custom Action', + desc: 'Reworked PHP function validation to use loopback-based fatal error checks and return cleaner syntax validation feedback.', + isPro: false + } + ] }, { label: __('Bug Fixes', 'bit-integrations'), @@ -56,23 +73,18 @@ const changeLog = [ itemClass: 'fixes-list', items: [ { - label: 'Blank page', - desc: 'Fixed blank page issue on integrations authorization screen across multiple integration field maps and triggers.', - isPro: false - }, - { - label: 'Redirect', - desc: 'Fixed redirect issue after saving a new integration.', - isPro: false + label: 'License', + desc: 'Fixed license expiry day calculation in the license status notice.', + isPro: true }, { - label: 'Style Breaks', - desc: 'Fixed style issue in the admin bar.', - isPro: false + label: 'License', + desc: 'Added backward-compatible removal for legacy license key option data.', + isPro: true }, { - label: 'Triggers Loading', - desc: 'Fixed active triggers loading issue', + label: 'Custom Trigger', + desc: 'Fixed custom trigger test data remove response key and success message labels.', isPro: true } ] diff --git a/readme.txt b/readme.txt index 740b2863..41a2c0f2 100644 --- a/readme.txt +++ b/readme.txt @@ -4,7 +4,7 @@ Tags: automation, automator, google sheets connector, zapier, WooCommerce Integr Requires at least: 5.1 Tested up to: 6.9 Requires PHP: 7.4 -Stable tag: 2.7.10 +Stable tag: 2.7.11 License: GPLv2 or later Perfect Automation and integration plugin: Connect 300+ platforms and automate CRM, Email marketing tools, Google Sheets, Contact forms, LMS and more @@ -718,6 +718,21 @@ Bit Integrations follows WordPress coding standards and best practices to ensure == Changelog == += 2.7.11 = +_Release Date - 26th February 2026_ + +- **New Features** + - WooCommerce: Booster for WooCommerce checkout fields added. + +- **Improvements** + - Custom Action: Reworked PHP function validation to use loopback-based fatal error checks and return cleaner syntax validation feedback. + - Breakdance: Updated Breakdance trigger test endpoints to use the shared trigger test routes (Pro). + +- **Bug Fixes** + - Fixed license expiry day calculation in the license status notice (Pro). + - Added backward-compatible removal for legacy license key option data (Pro). + - Fixed custom trigger test data remove response key and success message labels (Pro). + = 2.7.9 - 2.7.10 = _Release Date - 23rd February 2026_