From 9ba108869dcc871316f7e5e11aaf3f22774fa38e Mon Sep 17 00:00:00 2001 From: Faisal Ahammad Date: Mon, 25 May 2026 12:27:49 +0600 Subject: [PATCH 1/2] feat(plugin_repo): add trialware check Add new Trialware_Check under plugin_repo category to detect trialware and locked built-in functionality in plugin submissions. Scans PHP and JS files for five pattern groups: - License key gates - Pro/premium plan gates - Trial period expiration gates - Usage quota limits - Payment/subscription gates Each match reported as error with dedicated result code (trialware_{type}_candidate). Patterns designed to minimize false positives on legitimate external service integrations. Fixes #1313 --- docs/checks.md | 1 + .../Checks/Plugin_Repo/Trialware_Check.php | 194 ++++++++++++++++++ includes/Checker/Default_Check_Repository.php | 1 + .../load.php | 67 ++++++ .../load.php | 45 ++++ .../Checker/Checks/Trialware_Check_Tests.php | 80 ++++++++ 6 files changed, 388 insertions(+) create mode 100644 includes/Checker/Checks/Plugin_Repo/Trialware_Check.php create mode 100644 tests/phpunit/testdata/plugins/test-plugin-trialware-with-errors/load.php create mode 100644 tests/phpunit/testdata/plugins/test-plugin-trialware-without-errors/load.php create mode 100644 tests/phpunit/tests/Checker/Checks/Trialware_Check_Tests.php diff --git a/docs/checks.md b/docs/checks.md index 11662a607..2fd899202 100644 --- a/docs/checks.md +++ b/docs/checks.md @@ -35,3 +35,4 @@ | enqueued_styles_scope | performance | Checks whether any stylesheets are loaded on all pages, which is usually not desirable and can lead to performance issues. | [Learn more](https://developer.wordpress.org/plugins/) | | enqueued_scripts_scope | performance | Checks whether any scripts are loaded on all pages, which is usually not desirable and can lead to performance issues. | [Learn more](https://developer.wordpress.org/plugins/) | | non_blocking_scripts | performance | Checks whether scripts and styles are enqueued using a recommended loading strategy. | [Learn more](https://developer.wordpress.org/plugins/) | +| trialware | plugin_repo | Detects potential trialware or locked built-in functionality in plugins. | [Learn more](https://developer.wordpress.org/plugins/wordpress-org/detailed-plugin-guidelines/) | diff --git a/includes/Checker/Checks/Plugin_Repo/Trialware_Check.php b/includes/Checker/Checks/Plugin_Repo/Trialware_Check.php new file mode 100644 index 000000000..f3e3efd39 --- /dev/null +++ b/includes/Checker/Checks/Plugin_Repo/Trialware_Check.php @@ -0,0 +1,194 @@ + array( + 'patterns' => array( + '/(?:is_licensed|has_license|license_valid|check_license|verify_license)\s*\(/i', + '/(?:if\s*\(\s*!?\s*(?:license_key|license|license_status))/i', + '/license_key\s*(?:===|!==|==|!=)\s*[\'"](?:[a-zA-Z0-9_-]{10,}|FREE|TRIAL)[\'"]/i', + ), + 'code' => 'trialware_license_gate_candidate', + 'message' => 'Detected possible license key gate on plugin functionality.', + ), + 'pro_premium_gate' => array( + 'patterns' => array( + '/if\s*\(\s*!?\s*(?:is_pro|is_premium|has_pro|has_premium|pro_user|premium_user|is_paid)\s*\(/i', + '/if\s*\(\s*(?:!\s*)?(?:\\$this->|self::|static::)?(?:is_pro|is_premium|can_use_pro|pro_enabled|premium_enabled)\b/i', + '/(?:current_plan|user_plan|subscription_plan)\s*(?:===|!==|==|!=)\s*[\'"](?:pro|premium|business|enterprise)[\'"]/i', + ), + 'code' => 'trialware_pro_premium_gate_candidate', + 'message' => 'Detected possible pro/premium gate on plugin functionality.', + ), + 'trial_gate' => array( + 'patterns' => array( + '/if\s*\(\s*(?:!\s*)?(?:trial_expired|is_trial_over|trial_active|has_trial|in_trial|trial_days)\s*\(/i', + '/trial_(?:days|period|expires?|end|remaining)\s*(?:<=|>=|<|>|===|!==|==|!=)\s*\d/i', + '/(?:free_trial|trial_limit|trial_usage|trial_count)\s*(?:<=|>=|<|>|===|!==|==|!=)/i', + ), + 'code' => 'trialware_trial_gate_candidate', + 'message' => 'Detected possible trial period gate on plugin functionality.', + ), + 'quota_gate' => array( + 'patterns' => array( + '/if\s*\(\s*(?:!\s*)?(?:quota_exceeded|limit_reached|usage_limit|over_quota|at_limit|exceeds_limit)\s*\(/i', + '/if\s*\(\s*\\$\w*(?:usage|count|quota|limit)\s*(?:<=|>=|<|>|===|!==|==|!=)\s*\\$\w*(?:limit|max|quota|allowed)/i', + '/(?:upgrade_to|subscribe|purchase|buy)\s*\(\s*[\'"](?:pro|premium|paid|unlimited)[\'"]/i', + ), + 'code' => 'trialware_quota_gate_candidate', + 'message' => 'Detected possible usage quota gate on plugin functionality.', + ), + 'payment_gate' => array( + 'patterns' => array( + '/if\s*\(\s*(?:!\s*)?(?:has_paid|is_paid_user|payment_valid|subscription_active|is_subscribed)\s*\(/i', + '/(?:unlock|upgrade|go_pro|go_premium|get_pro|buy_pro)\s*\(\s*\)/i', + ), + 'code' => 'trialware_payment_gate_candidate', + 'message' => 'Detected possible payment gate on plugin functionality.', + ), + ); + + /** + * Gets the categories for the check. + * + * Every check must have at least one category. + * + * @since 1.4.0 + * + * @return array The categories for the check. + */ + public function get_categories() { + return array( Check_Categories::CATEGORY_PLUGIN_REPO ); + } + + /** + * Amends the given result by running the check on the given list of files. + * + * @since 1.4.0 + * + * @param Check_Result $result The check result to amend, including the plugin context to check. + * @param array $files List of absolute file paths. + * + * @throws Exception Thrown when the check fails with a critical error (unrelated to any errors detected as part of + * the check). + */ + protected function check_files( Check_Result $result, array $files ) { + $code_files = array_merge( + self::filter_files_by_extension( $files, 'php' ), + self::filter_files_by_extension( $files, 'js' ) + ); + + foreach ( self::PATTERN_GROUPS as $group ) { + $this->scan_for_pattern_group( $result, $code_files, $group ); + } + } + + /** + * Scans files for a pattern group and amends the result with matches. + * + * Each pattern in the group is checked independently. Matches are de-duplicated + * per file so the same file is not reported multiple times for the same group. + * + * @since 1.4.0 + * + * @param Check_Result $result The check result to amend. + * @param array $code_files List of absolute file paths. + * @param array $group Pattern group with 'patterns', 'code', and 'message' keys. + */ + private function scan_for_pattern_group( Check_Result $result, array $code_files, array $group ) { + $reported_files = array(); + + foreach ( $group['patterns'] as $pattern ) { + $files = self::files_preg_match_all( $pattern, $code_files ); + + if ( empty( $files ) ) { + continue; + } + + foreach ( $files as $file ) { + $file_path = $file['file']; + + // De-duplicate: report each file only once per group. + if ( isset( $reported_files[ $file_path ] ) ) { + continue; + } + + $reported_files[ $file_path ] = true; + + $this->add_result_error_for_file( + $result, + $group['message'], + $group['code'], + $file_path, + $file['line'], + $file['column'], + 'https://developer.wordpress.org/plugins/wordpress-org/detailed-plugin-guidelines/', + 7 + ); + } + } + } + + /** + * Gets the description for the check. + * + * Every check must have a short description explaining what the check does. + * + * @since 1.4.0 + * + * @return string Description. + */ + public function get_description(): string { + return __( 'Detects potential trialware or locked built-in functionality in plugins.', 'plugin-check' ); + } + + /** + * Gets the documentation URL for the check. + * + * Every check must have a URL with further information about the check. + * + * @since 1.4.0 + * + * @return string The documentation URL. + */ + public function get_documentation_url(): string { + return __( 'https://developer.wordpress.org/plugins/wordpress-org/detailed-plugin-guidelines/', 'plugin-check' ); + } +} diff --git a/includes/Checker/Default_Check_Repository.php b/includes/Checker/Default_Check_Repository.php index c22371044..8f878a62d 100644 --- a/includes/Checker/Default_Check_Repository.php +++ b/includes/Checker/Default_Check_Repository.php @@ -102,6 +102,7 @@ private function register_default_checks() { 'direct_file_access' => new Checks\Plugin_Repo\Direct_File_Access_Check(), 'external_admin_menu_links' => new Checks\Plugin_Repo\External_Admin_Menu_Links_Check(), 'wp_functions_compatibility' => new Checks\Plugin_Repo\WP_Functions_Compatibility_Check(), + 'trialware' => new Checks\Plugin_Repo\Trialware_Check(), ) ); diff --git a/tests/phpunit/testdata/plugins/test-plugin-trialware-with-errors/load.php b/tests/phpunit/testdata/plugins/test-plugin-trialware-with-errors/load.php new file mode 100644 index 000000000..3b17a9266 --- /dev/null +++ b/tests/phpunit/testdata/plugins/test-plugin-trialware-with-errors/load.php @@ -0,0 +1,67 @@ +'; + echo esc_html__( 'Settings Page', 'test-plugin' ); + echo ''; +} + +/** + * Save plugin settings. + */ +function test_trialware_save_settings() { + if ( ! isset( $_POST['test_nonce'] ) || ! wp_verify_nonce( sanitize_text_field( wp_unslash( $_POST['test_nonce'] ) ), 'test_action' ) ) { + return; + } + + $option = isset( $_POST['test_option'] ) ? sanitize_text_field( wp_unslash( $_POST['test_option'] ) ) : ''; + update_option( 'test_trialware_option', $option ); +} + +/** + * Load plugin textdomain. + */ +function test_trialware_load_textdomain() { + load_plugin_textdomain( 'test-plugin', false, dirname( plugin_basename( __FILE__ ) ) . '/languages' ); +} +add_action( 'plugins_loaded', 'test_trialware_load_textdomain' ); diff --git a/tests/phpunit/tests/Checker/Checks/Trialware_Check_Tests.php b/tests/phpunit/tests/Checker/Checks/Trialware_Check_Tests.php new file mode 100644 index 000000000..044eb7666 --- /dev/null +++ b/tests/phpunit/tests/Checker/Checks/Trialware_Check_Tests.php @@ -0,0 +1,80 @@ +run( $check_result ); + + $errors = $check_result->get_errors(); + + $this->assertNotEmpty( $errors ); + $this->assertSame( 5, $check_result->get_error_count() ); + + // Verify each result code is present. + $found_codes = array(); + foreach ( $errors as $file_errors ) { + foreach ( $file_errors as $line_errors ) { + foreach ( $line_errors as $col_errors ) { + foreach ( $col_errors as $message ) { + $found_codes[] = $message['code']; + } + } + } + } + + $this->assertContains( 'trialware_license_gate_candidate', $found_codes ); + $this->assertContains( 'trialware_pro_premium_gate_candidate', $found_codes ); + $this->assertContains( 'trialware_trial_gate_candidate', $found_codes ); + $this->assertContains( 'trialware_quota_gate_candidate', $found_codes ); + $this->assertContains( 'trialware_payment_gate_candidate', $found_codes ); + } + + public function test_trialware_without_errors() { + $check_context = new Check_Context( UNIT_TESTS_PLUGIN_DIR . 'test-plugin-trialware-without-errors/load.php' ); + $check_result = new Check_Result( $check_context ); + + $check = new Trialware_Check(); + $check->run( $check_result ); + + $errors = $check_result->get_errors(); + $warnings = $check_result->get_warnings(); + + $this->assertEmpty( $errors ); + $this->assertEmpty( $warnings ); + + $this->assertSame( 0, $check_result->get_error_count() ); + $this->assertSame( 0, $check_result->get_warning_count() ); + } + + public function test_trialware_check_is_plugin_repo_category() { + $check = new Trialware_Check(); + + $this->assertSame( array( 'plugin_repo' ), $check->get_categories() ); + } + + public function test_trialware_check_has_description() { + $check = new Trialware_Check(); + + $this->assertNotEmpty( $check->get_description() ); + } + + public function test_trialware_check_has_documentation_url() { + $check = new Trialware_Check(); + + $this->assertNotEmpty( $check->get_documentation_url() ); + $this->assertStringContainsString( 'https://', $check->get_documentation_url() ); + } +} From afe9a6f785c28d3581811a0b93db3122c7263721 Mon Sep 17 00:00:00 2001 From: Faisal Ahammad Date: Sat, 4 Jul 2026 22:33:15 +0600 Subject: [PATCH 2/2] fix(plugin_repo): route trialware candidates through existing AI review pass - add trialware_ prefix to AI_Analyzer::get_ai_prompt_map() - add prompts/ai-review-trialware.md defining genuine gate vs false positive - broaden Trialware_Check regex patterns (activation codes, free trial wording, to unlock/limit reached phrasing, lite-version gating) - add false-positive-shaped fixtures (external API key, separate premium plugin mention, EDD-style license) to the clean test plugin Addresses PR feedback from davidperezgar: candidates now flow through the same --ai runner pass already used by EscapeOutput/NonceVerification/Obfuscation/etc, instead of a new bespoke per-check AI workflow. Refs #1319 --- .../Checks/Plugin_Repo/Trialware_Check.php | 30 +++++++++--- includes/Traits/AI_Analyzer.php | 1 + prompts/ai-review-trialware.md | 14 ++++++ .../load.php | 46 +++++++++++++++++++ 4 files changed, 84 insertions(+), 7 deletions(-) create mode 100644 prompts/ai-review-trialware.md diff --git a/includes/Checker/Checks/Plugin_Repo/Trialware_Check.php b/includes/Checker/Checks/Plugin_Repo/Trialware_Check.php index f3e3efd39..ca349f6d2 100644 --- a/includes/Checker/Checks/Plugin_Repo/Trialware_Check.php +++ b/includes/Checker/Checks/Plugin_Repo/Trialware_Check.php @@ -20,7 +20,17 @@ * Scans code-like files for indicators that functionality shipped with the plugin * is gated behind license keys, trials, payment checks, quotas, or "pro" plan gates. * - * @since 1.4.0 + * All result codes produced by this check are AI-review candidates, not confirmed + * violations: regex matches are inherently prone to false positives (a separate premium + * product, an external service API key, harmless update-checker license wording, etc). + * Running Plugin Check with the `--ai` flag (or the AI toggle in the admin UI) triggers + * `AI_Analyzer::analyze_results_with_ai()` from + * {@see \WordPress\Plugin_Check\Checker\Abstract_Check_Runner::run()}, which reviews each + * `trialware_*` candidate against `prompts/ai-review-trialware.md` and tags it as a false + * positive when appropriate. This check intentionally does not call the AI client itself; + * it only produces candidates for that existing shared AI-review pass. + * + * @since 2.1.0 */ class Trialware_Check extends Abstract_File_Check { @@ -35,7 +45,7 @@ class Trialware_Check extends Abstract_File_Check { * - 'code': result code for matches in this group. * - 'message': human-readable description of what was detected. * - * @since 1.4.0 + * @since 2.1.0 * @var array */ const PATTERN_GROUPS = array( @@ -44,6 +54,7 @@ class Trialware_Check extends Abstract_File_Check { '/(?:is_licensed|has_license|license_valid|check_license|verify_license)\s*\(/i', '/(?:if\s*\(\s*!?\s*(?:license_key|license|license_status))/i', '/license_key\s*(?:===|!==|==|!=)\s*[\'"](?:[a-zA-Z0-9_-]{10,}|FREE|TRIAL)[\'"]/i', + '/(?:activation_code|activation_key)\s*(?:===|!==|==|!=)/i', ), 'code' => 'trialware_license_gate_candidate', 'message' => 'Detected possible license key gate on plugin functionality.', @@ -53,6 +64,8 @@ class Trialware_Check extends Abstract_File_Check { '/if\s*\(\s*!?\s*(?:is_pro|is_premium|has_pro|has_premium|pro_user|premium_user|is_paid)\s*\(/i', '/if\s*\(\s*(?:!\s*)?(?:\\$this->|self::|static::)?(?:is_pro|is_premium|can_use_pro|pro_enabled|premium_enabled)\b/i', '/(?:current_plan|user_plan|subscription_plan)\s*(?:===|!==|==|!=)\s*[\'"](?:pro|premium|business|enterprise)[\'"]/i', + '/if\s*\(\s*(?:!\s*)?(?:is_lite|is_free_version|free_version)\s*\(/i', + '/(?:only\s+available|available\s+only)\s+in\s+(?:the\s+)?(?:pro|premium)\s+version/i', ), 'code' => 'trialware_pro_premium_gate_candidate', 'message' => 'Detected possible pro/premium gate on plugin functionality.', @@ -62,6 +75,7 @@ class Trialware_Check extends Abstract_File_Check { '/if\s*\(\s*(?:!\s*)?(?:trial_expired|is_trial_over|trial_active|has_trial|in_trial|trial_days)\s*\(/i', '/trial_(?:days|period|expires?|end|remaining)\s*(?:<=|>=|<|>|===|!==|==|!=)\s*\d/i', '/(?:free_trial|trial_limit|trial_usage|trial_count)\s*(?:<=|>=|<|>|===|!==|==|!=)/i', + '/free\s+trial\s+(?:has\s+)?(?:ended|expired|is\s+over)/i', ), 'code' => 'trialware_trial_gate_candidate', 'message' => 'Detected possible trial period gate on plugin functionality.', @@ -71,6 +85,7 @@ class Trialware_Check extends Abstract_File_Check { '/if\s*\(\s*(?:!\s*)?(?:quota_exceeded|limit_reached|usage_limit|over_quota|at_limit|exceeds_limit)\s*\(/i', '/if\s*\(\s*\\$\w*(?:usage|count|quota|limit)\s*(?:<=|>=|<|>|===|!==|==|!=)\s*\\$\w*(?:limit|max|quota|allowed)/i', '/(?:upgrade_to|subscribe|purchase|buy)\s*\(\s*[\'"](?:pro|premium|paid|unlimited)[\'"]/i', + '/(?:plan|monthly|daily)\s+limit\s+(?:reached|exceeded)/i', ), 'code' => 'trialware_quota_gate_candidate', 'message' => 'Detected possible usage quota gate on plugin functionality.', @@ -79,6 +94,7 @@ class Trialware_Check extends Abstract_File_Check { 'patterns' => array( '/if\s*\(\s*(?:!\s*)?(?:has_paid|is_paid_user|payment_valid|subscription_active|is_subscribed)\s*\(/i', '/(?:unlock|upgrade|go_pro|go_premium|get_pro|buy_pro)\s*\(\s*\)/i', + '/to\s+unlock\s+(?:this|all|full)\s+(?:feature|features|functionality)/i', ), 'code' => 'trialware_payment_gate_candidate', 'message' => 'Detected possible payment gate on plugin functionality.', @@ -90,7 +106,7 @@ class Trialware_Check extends Abstract_File_Check { * * Every check must have at least one category. * - * @since 1.4.0 + * @since 2.1.0 * * @return array The categories for the check. */ @@ -101,7 +117,7 @@ public function get_categories() { /** * Amends the given result by running the check on the given list of files. * - * @since 1.4.0 + * @since 2.1.0 * * @param Check_Result $result The check result to amend, including the plugin context to check. * @param array $files List of absolute file paths. @@ -126,7 +142,7 @@ protected function check_files( Check_Result $result, array $files ) { * Each pattern in the group is checked independently. Matches are de-duplicated * per file so the same file is not reported multiple times for the same group. * - * @since 1.4.0 + * @since 2.1.0 * * @param Check_Result $result The check result to amend. * @param array $code_files List of absolute file paths. @@ -171,7 +187,7 @@ private function scan_for_pattern_group( Check_Result $result, array $code_files * * Every check must have a short description explaining what the check does. * - * @since 1.4.0 + * @since 2.1.0 * * @return string Description. */ @@ -184,7 +200,7 @@ public function get_description(): string { * * Every check must have a URL with further information about the check. * - * @since 1.4.0 + * @since 2.1.0 * * @return string The documentation URL. */ diff --git a/includes/Traits/AI_Analyzer.php b/includes/Traits/AI_Analyzer.php index 6dfb0a540..5e05d811a 100644 --- a/includes/Traits/AI_Analyzer.php +++ b/includes/Traits/AI_Analyzer.php @@ -84,6 +84,7 @@ protected function get_ai_prompt_map() { 'PluginCheck.CodeAnalysis.Obfuscation' => 'ai-review-code-obfuscation.md', 'PluginCheck.CodeAnalysis.SettingSanitization' => 'ai-review-setting-sanitization.md', 'PluginCheck.CodeAnalysis.PluginUpdater' => 'ai-review-plugin-updater.md', + 'trialware_' => 'ai-review-trialware.md', ); } diff --git a/prompts/ai-review-trialware.md b/prompts/ai-review-trialware.md new file mode 100644 index 000000000..dd11078b7 --- /dev/null +++ b/prompts/ai-review-trialware.md @@ -0,0 +1,14 @@ +## Trialware / Locked Functionality Issues + +A trialware issue occurs when functionality shipped in the plugin's own codebase is artificially restricted behind a license key, trial period, usage quota, "pro"/premium plan gate, or payment check, in violation of the WordPress.org guideline that plugins must be fully functional. + +Using the case as a reference, check the code to determine if it genuinely locks bundled functionality or if it is a false positive. + +Details: +- Genuinely flagged: code that checks a license/trial/quota/payment condition and then disables, hides, or short-circuits functionality that otherwise exists in the plugin's own files. +- A reference to a **separate, standalone premium plugin or add-on** sold by the same author (e.g. "Upgrade to Acme Pro" linking to a different plugin/product) is NOT trialware — that is a legitimate freemium business model, not locked bundled code. +- License-key checks used only to unlock **updates or support** (e.g. EDD/WooCommerce-style update-checker license activation) are NOT trialware, unless the same check also disables functionality already present in the submitted code. +- Checks against **external service API keys** (e.g. a third-party SaaS API key required to call that external service) are NOT trialware — the plugin isn't restricting its own bundled code, it's authenticating to an external system it doesn't control. +- Generic marketing copy, readme wording, or UI strings that merely mention "premium", "pro", or "trial" without any corresponding code path that disables bundled functionality are NOT trialware. +- Quota/limit checks tied to an **external resource** (API rate limits, storage quotas on a remote service) are NOT trialware; quota checks that disable the plugin's own local features once a count is reached ARE trialware. +- If the flagged code is inside test fixtures, examples, or clearly non-executed sample code, it is a false positive. diff --git a/tests/phpunit/testdata/plugins/test-plugin-trialware-without-errors/load.php b/tests/phpunit/testdata/plugins/test-plugin-trialware-without-errors/load.php index 621815c85..52ddf5909 100644 --- a/tests/phpunit/testdata/plugins/test-plugin-trialware-without-errors/load.php +++ b/tests/phpunit/testdata/plugins/test-plugin-trialware-without-errors/load.php @@ -43,3 +43,49 @@ function test_trialware_load_textdomain() { load_plugin_textdomain( 'test-plugin', false, dirname( plugin_basename( __FILE__ ) ) . '/languages' ); } add_action( 'plugins_loaded', 'test_trialware_load_textdomain' ); + +/** + * Calls a third-party weather API using its own API key. + * + * Not trialware: the key authenticates to an external service the plugin + * doesn't control, it does not gate any of the plugin's bundled functionality. + */ +function test_trialware_call_external_api() { + $api_key = get_option( 'test_weather_api_key' ); + + if ( empty( $api_key ) ) { + return new WP_Error( 'missing_api_key', __( 'Weather API key is required.', 'test-plugin' ) ); + } + + return wp_remote_get( 'https://api.example.com/weather?key=' . $api_key ); +} + +/** + * Renders a notice linking to a separate premium add-on plugin. + * + * Not trialware: this promotes a standalone product, it doesn't disable + * any functionality bundled in this plugin. + */ +function test_trialware_upgrade_notice() { + echo '

' . esc_html__( 'Need more features? Check out Test Plugin Pro, our premium add-on.', 'test-plugin' ) . '

'; + echo '' . esc_html__( 'Learn more', 'test-plugin' ) . ''; +} + +/** + * Checks for plugin updates using a license key, EDD-style updater pattern. + * + * Not trialware: the license only unlocks update/support delivery, it does + * not disable any bundled functionality. + */ +function test_trialware_check_for_updates() { + $updater = new Test_Plugin_Updater( + 'https://example.com', + __FILE__, + array( + 'license' => get_option( 'test_plugin_license_key' ), + 'item_id' => 12345, + ) + ); + + $updater->fetch_update_data(); +}