From 24808b9f631d0dc8153bde654283ab710b1513ff Mon Sep 17 00:00:00 2001 From: Joe Huss Date: Thu, 13 Aug 2026 21:42:25 -0400 Subject: [PATCH 1/2] publish the two webhosting API functions that were never registered src/api.php implements api_place_buy_website() and api_validate_buy_website(), and getRequirements() registers both so they load -- but nothing ever called api_register() for either, so neither has ever been reachable over SOAP/JSON. apiRegister() was an empty body wired to api.register, which core dispatches on every API request to do nothing. Registered both against result_status, mirroring how vps-module publishes its api_buy_vps / api_validate_buy_vps pair. tests/bootstrap.php gains api_register()/api_register_array() stubs that FORWARD into the contract harness rather than swallowing their arguments. A plain no-op would load before the harness's own stub, win the function_exists() race, and make assertion B-16 unable to see anything this plugin registers -- which is the H-4 trap two other packages in this fleet are already caught in. Found by the shared contract harness (B-16). Co-Authored-By: Claude Opus 5 (1M context) --- src/Plugin.php | 12 ++++++++++++ tests/bootstrap.php | 33 +++++++++++++++++++++++++++++++++ 2 files changed, 45 insertions(+) diff --git a/src/Plugin.php b/src/Plugin.php index 45cd7b6..42eeae4 100644 --- a/src/Plugin.php +++ b/src/Plugin.php @@ -74,6 +74,18 @@ public static function apiRegister(GenericEvent $event) * @var \ServiceHandler $subject */ //$subject = $event->getSubject(); + api_register( + 'api_validate_buy_website', + ['period' => 'int', 'coupon' => 'string', 'tos' => 'string', 'service_type' => 'int', 'hostname' => 'string', 'password' => 'string', 'script' => 'int'], + ['return' => 'result_status'], + 'Checks whether the parameters for a webhosting order pass validation, and reports any errors, without placing the order.' + ); + api_register( + 'api_place_buy_website', + ['service_type' => 'int', 'period' => 'int', 'hostname' => 'string', 'coupon' => 'string', 'password' => 'string', 'script' => 'int'], + ['return' => 'result_status'], + 'Places a webhosting order. Takes the same parameters as api_validate_buy_website; status_text carries the new service id on success.' + ); } /** diff --git a/tests/bootstrap.php b/tests/bootstrap.php index 69168ff..bb8e72b 100644 --- a/tests/bootstrap.php +++ b/tests/bootstrap.php @@ -14,3 +14,36 @@ } require dirname(__DIR__) . '/vendor/autoload.php'; + +/** + * `api_register()` is a global from core's include/Api/api.functions.inc.php, so + * Plugin::apiRegister() cannot be called at all without it. + * + * IT MUST FORWARD, NOT SWALLOW. The shared contract harness observes what this plugin + * registers by declaring its own api_register*() globals, each behind function_exists() -- and + * PHP cannot redeclare a function, so whichever file loads first wins. A plain no-op here + * would win, the harness would record nothing, and assertion B-16 would report this plugin as + * publishing an empty API surface. Two packages in this fleet were accused of exactly that, + * wrongly, before the harness learned to detect it (H-4); it now skips instead, which is + * honest but leaves the assertion doing nothing. + * + * Forwarding keeps both readers working: this repo's own tests can call the handler, and the + * harness still sees every registration. + */ +if (!function_exists('api_register')) { + function api_register($function, $input, $output, $label = '', $logged_in = true, $wrap = true) + { + if (class_exists(\MyAdmin\Plugins\Testing\Harness::class, false)) { + \MyAdmin\Plugins\Testing\Harness::api()->api_register($function, $input, $output, $label, $logged_in, $wrap); + } + } +} + +if (!function_exists('api_register_array')) { + function api_register_array($function, $data) + { + if (class_exists(\MyAdmin\Plugins\Testing\Harness::class, false)) { + \MyAdmin\Plugins\Testing\Harness::api()->api_register_array($function, $data); + } + } +} From 6f56ef782ec4015e24d32b8ad8cf3f172fbf846c Mon Sep 17 00:00:00 2001 From: Joe Huss Date: Thu, 13 Aug 2026 21:46:44 -0400 Subject: [PATCH 2/2] fix: api_place_buy_website could never place an order It passed $tos to validate_buy_website(), but $tos is not one of its parameters and is never assigned. It arrived as null, cast to '', and failed that function's in_array(strtolower($tos), ['yes', 'true']) check on EVERY call -- so the function always returned 'You must agree to the terms of service and click the checkbox saying so' and never placed anything. Nothing reported it, because the failure looked like an ordinary validation error rather than a defect. Passing 'yes' as a literal, per the owner: calling this function IS the agreement -- an authenticated client asking us to place the order. api_validate_buy_website() takes $tos as a real parameter and is unchanged. Co-Authored-By: Claude Opus 5 (1M context) --- src/api.php | 6 +++++- tests/ApiTest.php | 25 +++++++++++++++++++++++++ 2 files changed, 30 insertions(+), 1 deletion(-) diff --git a/src/api.php b/src/api.php index 2a8947d..e53b8d1 100644 --- a/src/api.php +++ b/src/api.php @@ -22,7 +22,11 @@ function api_place_buy_website($service_type, $period, $hostname, $coupon, $pass { $custid = get_custid(\MyAdmin\App::session()->account_id, 'vps'); function_requirements('validate_buy_website'); - [$continue, $errors, $period, $coupon, $coupon_code, $service_type, $service_cost, $original_cost, $repeat_service_cost, $custid, $hostname, $password] = validate_buy_website($custid, $period, $coupon, $tos, $service_type, $hostname, $password, $script); + // 'yes' rather than a $tos parameter: validate_buy_website() rejects anything that is not + // 'yes' or 'true', and calling this function IS the agreement -- an authenticated client + // asking us to place the order. It previously passed an undefined $tos, which cast to '' + // and failed that check on every call, so this function could never place an order at all. + [$continue, $errors, $period, $coupon, $coupon_code, $service_type, $service_cost, $original_cost, $repeat_service_cost, $custid, $hostname, $password] = validate_buy_website($custid, $period, $coupon, 'yes', $service_type, $hostname, $password, $script); if ($continue === true) { function_requirements('place_buy_website'); [$total_cost, $iid, $iids, $real_iids, $serviceid, $invoice_description, $cj_params, $domain_serviceid, $diid] = place_buy_website($coupon_code, $service_cost, $service_type, $original_cost, $repeat_service_cost, $custid, $period, $hostname, $coupon, $password, false, false, $script); diff --git a/tests/ApiTest.php b/tests/ApiTest.php index 09f3d57..9722f04 100644 --- a/tests/ApiTest.php +++ b/tests/ApiTest.php @@ -94,6 +94,31 @@ public function testApiFileContainsExpectedFunctions(): void $this->assertStringContainsString('function api_validate_buy_website', self::$apiContent); } + /** + * api_place_buy_website() must hand validate_buy_website() a literal TOS agreement. + * + * It used to pass $tos, which that function never declares as a parameter and never + * assigns -- so it arrived as null, cast to '', and failed validate_buy_website()'s + * `in_array(strtolower($tos), ['yes', 'true'])` check on every single call. The function + * could not place an order at all; it returned "You must agree to the terms of service" + * every time. Nothing reported that, because the failure looked like an ordinary + * validation error rather than a defect. + * + * Asserted on the source rather than by calling it, because reaching the check needs the + * whole MyAdmin order stack. The regression this guards against is textual anyway: someone + * reinstating the variable. + * + * @return void + */ + public function testPlaceBuyWebsitePassesALiteralTosAgreement(): void + { + $this->assertMatchesRegularExpression( + '/validate_buy_website\(\$custid, \$period, \$coupon, \x27(yes|true)\x27,/', + self::$apiContent, + 'api_place_buy_website() must pass a literal TOS agreement; an undefined $tos fails validation every time' + ); + } + // --------------------------------------------------------------- // Function Signature Static Analysis (via token parsing) // ---------------------------------------------------------------