From e74a9978dab56295cc6d77a90e56a917ad59682b Mon Sep 17 00:00:00 2001 From: Dat Date: Thu, 18 Jun 2026 13:46:48 +0200 Subject: [PATCH 1/8] update guzzlehttp/guzzle to 7.12 and add test to prove Guzzle rejects bare hostnames --- composer.json | 2 +- composer.lock | 50 ++++++++++---------- tests/Services/MediaWikiHostResolverTest.php | 34 +++++++++++++ 3 files changed, 61 insertions(+), 25 deletions(-) diff --git a/composer.json b/composer.json index 782234cb..188d1f36 100644 --- a/composer.json +++ b/composer.json @@ -11,7 +11,7 @@ "absszero/laravel-stackdriver-error-reporting": "^1.9", "firebase/php-jwt": "^7.0", "google/recaptcha": "^1.2", - "guzzlehttp/guzzle": "^7.8", + "guzzlehttp/guzzle": "^7.12", "guzzlehttp/psr7": "^2.9", "hackzilla/password-generator": "^1.6", "intervention/image": "^2.5", diff --git a/composer.lock b/composer.lock index 2f7328f4..2965d747 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "b53936f6dbe98d5646390f1d2e1fe647", + "content-hash": "518d96c6fc47a13344b85147f2f93526", "packages": [ { "name": "absszero/laravel-stackdriver-error-reporting", @@ -1691,25 +1691,26 @@ }, { "name": "guzzlehttp/guzzle", - "version": "7.10.5", + "version": "7.12.0", "source": { "type": "git", "url": "https://github.com/guzzle/guzzle.git", - "reference": "7c8d84b39e680315f687e8662a9d6fb0865c5148" + "reference": "eaa81598031cf57a9e36258c8546defffc994cba" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/guzzle/guzzle/zipball/7c8d84b39e680315f687e8662a9d6fb0865c5148", - "reference": "7c8d84b39e680315f687e8662a9d6fb0865c5148", + "url": "https://api.github.com/repos/guzzle/guzzle/zipball/eaa81598031cf57a9e36258c8546defffc994cba", + "reference": "eaa81598031cf57a9e36258c8546defffc994cba", "shasum": "" }, "require": { "ext-json": "*", - "guzzlehttp/promises": "^2.3", - "guzzlehttp/psr7": "^2.8", + "guzzlehttp/promises": "^2.5", + "guzzlehttp/psr7": "^2.12", "php": "^7.2.5 || ^8.0", "psr/http-client": "^1.0", - "symfony/deprecation-contracts": "^2.2 || ^3.0" + "symfony/deprecation-contracts": "^2.5 || ^3.0", + "symfony/polyfill-php80": "^1.24" }, "provide": { "psr/http-client-implementation": "1.0" @@ -1718,7 +1719,7 @@ "bamarni/composer-bin-plugin": "^1.8.2", "ext-curl": "*", "guzzle/client-integration-tests": "3.0.2", - "guzzlehttp/test-server": "^0.4", + "guzzlehttp/test-server": "^0.5", "php-http/message-factory": "^1.1", "phpunit/phpunit": "^8.5.52 || ^9.6.34", "psr/log": "^1.1 || ^2.0 || ^3.0" @@ -1798,7 +1799,7 @@ ], "support": { "issues": "https://github.com/guzzle/guzzle/issues", - "source": "https://github.com/guzzle/guzzle/tree/7.10.5" + "source": "https://github.com/guzzle/guzzle/tree/7.12.0" }, "funding": [ { @@ -1814,24 +1815,25 @@ "type": "tidelift" } ], - "time": "2026-05-27T11:53:46+00:00" + "time": "2026-06-16T22:11:48+00:00" }, { "name": "guzzlehttp/promises", - "version": "2.4.1", + "version": "2.5.0", "source": { "type": "git", "url": "https://github.com/guzzle/promises.git", - "reference": "09e8a212562fb1fb6a512c4156ed71525969d6c2" + "reference": "4360e982f87f5f258bf872d094647791db2f4c8e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/guzzle/promises/zipball/09e8a212562fb1fb6a512c4156ed71525969d6c2", - "reference": "09e8a212562fb1fb6a512c4156ed71525969d6c2", + "url": "https://api.github.com/repos/guzzle/promises/zipball/4360e982f87f5f258bf872d094647791db2f4c8e", + "reference": "4360e982f87f5f258bf872d094647791db2f4c8e", "shasum": "" }, "require": { - "php": "^7.2.5 || ^8.0" + "php": "^7.2.5 || ^8.0", + "symfony/deprecation-contracts": "^2.5 || ^3.0" }, "require-dev": { "bamarni/composer-bin-plugin": "^1.8.2", @@ -1881,7 +1883,7 @@ ], "support": { "issues": "https://github.com/guzzle/promises/issues", - "source": "https://github.com/guzzle/promises/tree/2.4.1" + "source": "https://github.com/guzzle/promises/tree/2.5.0" }, "funding": [ { @@ -1897,20 +1899,20 @@ "type": "tidelift" } ], - "time": "2026-05-20T22:57:30+00:00" + "time": "2026-06-02T12:23:43+00:00" }, { "name": "guzzlehttp/psr7", - "version": "2.11.1", + "version": "2.12.1", "source": { "type": "git", "url": "https://github.com/guzzle/psr7.git", - "reference": "640e2897bbee822dbc8af761d49e1a29b1f2a6b1" + "reference": "172ef2f4e9824c1e058b7f30be8ae25a02c0f2b7" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/guzzle/psr7/zipball/640e2897bbee822dbc8af761d49e1a29b1f2a6b1", - "reference": "640e2897bbee822dbc8af761d49e1a29b1f2a6b1", + "url": "https://api.github.com/repos/guzzle/psr7/zipball/172ef2f4e9824c1e058b7f30be8ae25a02c0f2b7", + "reference": "172ef2f4e9824c1e058b7f30be8ae25a02c0f2b7", "shasum": "" }, "require": { @@ -2000,7 +2002,7 @@ ], "support": { "issues": "https://github.com/guzzle/psr7/issues", - "source": "https://github.com/guzzle/psr7/tree/2.11.1" + "source": "https://github.com/guzzle/psr7/tree/2.12.1" }, "funding": [ { @@ -2016,7 +2018,7 @@ "type": "tidelift" } ], - "time": "2026-06-12T21:50:12+00:00" + "time": "2026-06-18T09:49:37+00:00" }, { "name": "guzzlehttp/uri-template", diff --git a/tests/Services/MediaWikiHostResolverTest.php b/tests/Services/MediaWikiHostResolverTest.php index 77c982bc..77d993bb 100644 --- a/tests/Services/MediaWikiHostResolverTest.php +++ b/tests/Services/MediaWikiHostResolverTest.php @@ -8,7 +8,10 @@ use App\Wiki; use App\WikiDb; use Faker\Factory; +use GuzzleHttp\Client; +use GuzzleHttp\Exception\RequestException; use Illuminate\Foundation\Testing\RefreshDatabase; +use Illuminate\Support\Facades\Http; class MediaWikiHostResolverTest extends TestCase { use RefreshDatabase; @@ -23,6 +26,16 @@ public function testResolverRoutesToCorrectHost(): void { ); } + public function testResolverBuildsBackendUrl(): void { + $domain = (new Factory())->create()->unique()->text(30); + $this->createWiki($domain, 'mw1.43-wbs2'); + $resolver = new MediaWikiHostResolver(); + $this->assertEquals( + 'http://mediawiki-143-app-backend.default.svc.cluster.local', + $resolver->getBackendHostForDomain($domain) + ); + } + private function createWiki(string $domain, string $version) { $wiki = Wiki::factory()->create(['domain' => $domain]); WikiDb::create([ @@ -53,4 +66,25 @@ public function testResolverThrowsIfUnableToFindWiki(): void { UnknownWikiDomainException::class ); } + + public function testGuzzleRejectsSchemelesUrls(): void { + $client = new Client(); + + // This should throw RequestException about empty scheme + $this->assertThrows( + fn () => $client->get('mediawiki-143-app-backend.default.svc.cluster.local/w/api.php'), + RequestException::class, + 'scheme "" is not allowed' + ); + } + + public function testGuzzleAcceptsSchemeUrls(): void { + Http::fake(['*' => Http::response()]); + + $client = new Client(); + // This should NOT throw + $response = $client->get('http://mediawiki-143-app-backend.default.svc.cluster.local/w/api.php'); + + $this->assertTrue($response->getStatusCode() === 200); + } } From 373a022ae07aa4364c9bd1461f037a149063f9b9 Mon Sep 17 00:00:00 2001 From: Dat Date: Fri, 26 Jun 2026 10:56:28 +0200 Subject: [PATCH 2/8] Patch resolver with explicit URL --- app/Services/MediaWikiHostResolver.php | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/app/Services/MediaWikiHostResolver.php b/app/Services/MediaWikiHostResolver.php index c8e6454f..478a41c1 100644 --- a/app/Services/MediaWikiHostResolver.php +++ b/app/Services/MediaWikiHostResolver.php @@ -37,6 +37,10 @@ public function getBackendHostForDomain(string $domain): string { return sprintf('mediawiki-%s-app-backend.default.svc.cluster.local', $this->getMwVersionForDomain($domain)); } + public function getBackendUrlForDomain(string $domain): string { + return 'http://' . $this->getBackendHostForDomain($domain); + } + private function getMwVersionForDomain(string $domain): string { $wiki = Wiki::where('domain', $domain)->first(); From 217a152b5ea2d44efa183e194ff722f682151290 Mon Sep 17 00:00:00 2001 From: Dat Date: Thu, 18 Jun 2026 13:46:48 +0200 Subject: [PATCH 3/8] update guzzlehttp/guzzle to 7.12 and add test to prove Guzzle rejects bare hostnames --- composer.json | 2 +- composer.lock | 50 ++++++++++---------- tests/Services/MediaWikiHostResolverTest.php | 34 +++++++++++++ 3 files changed, 61 insertions(+), 25 deletions(-) diff --git a/composer.json b/composer.json index 782234cb..188d1f36 100644 --- a/composer.json +++ b/composer.json @@ -11,7 +11,7 @@ "absszero/laravel-stackdriver-error-reporting": "^1.9", "firebase/php-jwt": "^7.0", "google/recaptcha": "^1.2", - "guzzlehttp/guzzle": "^7.8", + "guzzlehttp/guzzle": "^7.12", "guzzlehttp/psr7": "^2.9", "hackzilla/password-generator": "^1.6", "intervention/image": "^2.5", diff --git a/composer.lock b/composer.lock index 2f7328f4..2965d747 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "b53936f6dbe98d5646390f1d2e1fe647", + "content-hash": "518d96c6fc47a13344b85147f2f93526", "packages": [ { "name": "absszero/laravel-stackdriver-error-reporting", @@ -1691,25 +1691,26 @@ }, { "name": "guzzlehttp/guzzle", - "version": "7.10.5", + "version": "7.12.0", "source": { "type": "git", "url": "https://github.com/guzzle/guzzle.git", - "reference": "7c8d84b39e680315f687e8662a9d6fb0865c5148" + "reference": "eaa81598031cf57a9e36258c8546defffc994cba" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/guzzle/guzzle/zipball/7c8d84b39e680315f687e8662a9d6fb0865c5148", - "reference": "7c8d84b39e680315f687e8662a9d6fb0865c5148", + "url": "https://api.github.com/repos/guzzle/guzzle/zipball/eaa81598031cf57a9e36258c8546defffc994cba", + "reference": "eaa81598031cf57a9e36258c8546defffc994cba", "shasum": "" }, "require": { "ext-json": "*", - "guzzlehttp/promises": "^2.3", - "guzzlehttp/psr7": "^2.8", + "guzzlehttp/promises": "^2.5", + "guzzlehttp/psr7": "^2.12", "php": "^7.2.5 || ^8.0", "psr/http-client": "^1.0", - "symfony/deprecation-contracts": "^2.2 || ^3.0" + "symfony/deprecation-contracts": "^2.5 || ^3.0", + "symfony/polyfill-php80": "^1.24" }, "provide": { "psr/http-client-implementation": "1.0" @@ -1718,7 +1719,7 @@ "bamarni/composer-bin-plugin": "^1.8.2", "ext-curl": "*", "guzzle/client-integration-tests": "3.0.2", - "guzzlehttp/test-server": "^0.4", + "guzzlehttp/test-server": "^0.5", "php-http/message-factory": "^1.1", "phpunit/phpunit": "^8.5.52 || ^9.6.34", "psr/log": "^1.1 || ^2.0 || ^3.0" @@ -1798,7 +1799,7 @@ ], "support": { "issues": "https://github.com/guzzle/guzzle/issues", - "source": "https://github.com/guzzle/guzzle/tree/7.10.5" + "source": "https://github.com/guzzle/guzzle/tree/7.12.0" }, "funding": [ { @@ -1814,24 +1815,25 @@ "type": "tidelift" } ], - "time": "2026-05-27T11:53:46+00:00" + "time": "2026-06-16T22:11:48+00:00" }, { "name": "guzzlehttp/promises", - "version": "2.4.1", + "version": "2.5.0", "source": { "type": "git", "url": "https://github.com/guzzle/promises.git", - "reference": "09e8a212562fb1fb6a512c4156ed71525969d6c2" + "reference": "4360e982f87f5f258bf872d094647791db2f4c8e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/guzzle/promises/zipball/09e8a212562fb1fb6a512c4156ed71525969d6c2", - "reference": "09e8a212562fb1fb6a512c4156ed71525969d6c2", + "url": "https://api.github.com/repos/guzzle/promises/zipball/4360e982f87f5f258bf872d094647791db2f4c8e", + "reference": "4360e982f87f5f258bf872d094647791db2f4c8e", "shasum": "" }, "require": { - "php": "^7.2.5 || ^8.0" + "php": "^7.2.5 || ^8.0", + "symfony/deprecation-contracts": "^2.5 || ^3.0" }, "require-dev": { "bamarni/composer-bin-plugin": "^1.8.2", @@ -1881,7 +1883,7 @@ ], "support": { "issues": "https://github.com/guzzle/promises/issues", - "source": "https://github.com/guzzle/promises/tree/2.4.1" + "source": "https://github.com/guzzle/promises/tree/2.5.0" }, "funding": [ { @@ -1897,20 +1899,20 @@ "type": "tidelift" } ], - "time": "2026-05-20T22:57:30+00:00" + "time": "2026-06-02T12:23:43+00:00" }, { "name": "guzzlehttp/psr7", - "version": "2.11.1", + "version": "2.12.1", "source": { "type": "git", "url": "https://github.com/guzzle/psr7.git", - "reference": "640e2897bbee822dbc8af761d49e1a29b1f2a6b1" + "reference": "172ef2f4e9824c1e058b7f30be8ae25a02c0f2b7" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/guzzle/psr7/zipball/640e2897bbee822dbc8af761d49e1a29b1f2a6b1", - "reference": "640e2897bbee822dbc8af761d49e1a29b1f2a6b1", + "url": "https://api.github.com/repos/guzzle/psr7/zipball/172ef2f4e9824c1e058b7f30be8ae25a02c0f2b7", + "reference": "172ef2f4e9824c1e058b7f30be8ae25a02c0f2b7", "shasum": "" }, "require": { @@ -2000,7 +2002,7 @@ ], "support": { "issues": "https://github.com/guzzle/psr7/issues", - "source": "https://github.com/guzzle/psr7/tree/2.11.1" + "source": "https://github.com/guzzle/psr7/tree/2.12.1" }, "funding": [ { @@ -2016,7 +2018,7 @@ "type": "tidelift" } ], - "time": "2026-06-12T21:50:12+00:00" + "time": "2026-06-18T09:49:37+00:00" }, { "name": "guzzlehttp/uri-template", diff --git a/tests/Services/MediaWikiHostResolverTest.php b/tests/Services/MediaWikiHostResolverTest.php index 77c982bc..77d993bb 100644 --- a/tests/Services/MediaWikiHostResolverTest.php +++ b/tests/Services/MediaWikiHostResolverTest.php @@ -8,7 +8,10 @@ use App\Wiki; use App\WikiDb; use Faker\Factory; +use GuzzleHttp\Client; +use GuzzleHttp\Exception\RequestException; use Illuminate\Foundation\Testing\RefreshDatabase; +use Illuminate\Support\Facades\Http; class MediaWikiHostResolverTest extends TestCase { use RefreshDatabase; @@ -23,6 +26,16 @@ public function testResolverRoutesToCorrectHost(): void { ); } + public function testResolverBuildsBackendUrl(): void { + $domain = (new Factory())->create()->unique()->text(30); + $this->createWiki($domain, 'mw1.43-wbs2'); + $resolver = new MediaWikiHostResolver(); + $this->assertEquals( + 'http://mediawiki-143-app-backend.default.svc.cluster.local', + $resolver->getBackendHostForDomain($domain) + ); + } + private function createWiki(string $domain, string $version) { $wiki = Wiki::factory()->create(['domain' => $domain]); WikiDb::create([ @@ -53,4 +66,25 @@ public function testResolverThrowsIfUnableToFindWiki(): void { UnknownWikiDomainException::class ); } + + public function testGuzzleRejectsSchemelesUrls(): void { + $client = new Client(); + + // This should throw RequestException about empty scheme + $this->assertThrows( + fn () => $client->get('mediawiki-143-app-backend.default.svc.cluster.local/w/api.php'), + RequestException::class, + 'scheme "" is not allowed' + ); + } + + public function testGuzzleAcceptsSchemeUrls(): void { + Http::fake(['*' => Http::response()]); + + $client = new Client(); + // This should NOT throw + $response = $client->get('http://mediawiki-143-app-backend.default.svc.cluster.local/w/api.php'); + + $this->assertTrue($response->getStatusCode() === 200); + } } From 5287a4af4a7d5df55b70e0c0cc78ad36b1d0df7c Mon Sep 17 00:00:00 2001 From: Dat Date: Fri, 26 Jun 2026 10:56:28 +0200 Subject: [PATCH 4/8] Patch resolver with explicit URL --- app/Services/MediaWikiHostResolver.php | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/app/Services/MediaWikiHostResolver.php b/app/Services/MediaWikiHostResolver.php index c8e6454f..478a41c1 100644 --- a/app/Services/MediaWikiHostResolver.php +++ b/app/Services/MediaWikiHostResolver.php @@ -37,6 +37,10 @@ public function getBackendHostForDomain(string $domain): string { return sprintf('mediawiki-%s-app-backend.default.svc.cluster.local', $this->getMwVersionForDomain($domain)); } + public function getBackendUrlForDomain(string $domain): string { + return 'http://' . $this->getBackendHostForDomain($domain); + } + private function getMwVersionForDomain(string $domain): string { $wiki = Wiki::where('domain', $domain)->first(); From d7f5ebc090edab6af82353443ed5e20b56efec79 Mon Sep 17 00:00:00 2001 From: Dat Date: Fri, 26 Jun 2026 12:42:40 +0200 Subject: [PATCH 5/8] Added getBackendUrlForDomain() method returning URLs with explicit http:// --- app/Console/Commands/RebuildQueryserviceData.php | 2 +- app/Jobs/CirrusSearch/CirrusSearchJob.php | 2 +- app/Jobs/MediawikiInit.php | 2 +- app/Jobs/MediawikiSandboxLoadData.php | 2 +- app/Jobs/PlatformStatsSummaryJob.php | 2 +- app/Jobs/PollForMediaWikiJobsJob.php | 2 +- app/Jobs/SiteStatsUpdateJob.php | 2 +- app/Jobs/UpdateWikiSiteStatsJob.php | 10 +++++----- app/Jobs/WikiEntityImportJob.php | 2 +- 9 files changed, 13 insertions(+), 13 deletions(-) diff --git a/app/Console/Commands/RebuildQueryserviceData.php b/app/Console/Commands/RebuildQueryserviceData.php index 8286950e..fbc9308f 100644 --- a/app/Console/Commands/RebuildQueryserviceData.php +++ b/app/Console/Commands/RebuildQueryserviceData.php @@ -42,7 +42,7 @@ public function handle(MediaWikiHostResolver $mwHostResolver) { $skippedWikis = 0; $processedWikis = 0; foreach ($wikis as $wiki) { - $this->apiUrl = $mwHostResolver->getBackendHostForDomain($wiki->domain) . '/w/api.php'; // used in PageFetcher::fetchPagesInNamespace + $this->apiUrl = $mwHostResolver->getBackendUrlForDomain($wiki->domain) . '/w/api.php'; // used in PageFetcher::fetchPagesInNamespace try { $entities = $this->getEntitiesForWiki($wiki); diff --git a/app/Jobs/CirrusSearch/CirrusSearchJob.php b/app/Jobs/CirrusSearch/CirrusSearchJob.php index ce6d6bad..95f35c10 100644 --- a/app/Jobs/CirrusSearch/CirrusSearchJob.php +++ b/app/Jobs/CirrusSearch/CirrusSearchJob.php @@ -70,7 +70,7 @@ public function handle(HttpRequest $request, MediaWikiHostResolver $mwHostResolv $request->setOptions( [ - CURLOPT_URL => $mwHostResolver->getBackendHostForDomain($this->wiki->domain) . '/w/api.php?action=' . $this->apiModule() . $this->getQueryParams(), + CURLOPT_URL => $mwHostResolver->getBackendUrlForDomain($this->wiki->domain) . '/w/api.php?action=' . $this->apiModule() . $this->getQueryParams(), CURLOPT_RETURNTRANSFER => true, CURLOPT_ENCODING => '', CURLOPT_TIMEOUT => $this->getRequestTimeout(), diff --git a/app/Jobs/MediawikiInit.php b/app/Jobs/MediawikiInit.php index 76bb5b4e..6922923a 100644 --- a/app/Jobs/MediawikiInit.php +++ b/app/Jobs/MediawikiInit.php @@ -31,7 +31,7 @@ public function handle(HttpRequest $request, MediaWikiHostResolver $mwHostResolv ]; $request->setOptions([ - CURLOPT_URL => $mwHostResolver->getBackendHostForDomain($this->wikiDomain) . '/w/api.php?action=wbstackInit&format=json', + CURLOPT_URL => $mwHostResolver->getBackendUrlForDomain($this->wikiDomain) . '/w/api.php?action=wbstackInit&format=json', CURLOPT_RETURNTRANSFER => true, CURLOPT_ENCODING => '', CURLOPT_TIMEOUT => 60, diff --git a/app/Jobs/MediawikiSandboxLoadData.php b/app/Jobs/MediawikiSandboxLoadData.php index 8d294432..d1e3940e 100644 --- a/app/Jobs/MediawikiSandboxLoadData.php +++ b/app/Jobs/MediawikiSandboxLoadData.php @@ -27,7 +27,7 @@ public function handle(MediaWikiHostResolver $mwHostResolver) { $curl = curl_init(); curl_setopt_array($curl, [ - CURLOPT_URL => $mwHostResolver->getBackendHostForDomain($this->wikiDomain) . '/w/rest.php/wikibase-exampledata/v0/load', + CURLOPT_URL => $mwHostResolver->getBackendUrlForDomain($this->wikiDomain) . '/w/rest.php/wikibase-exampledata/v0/load', CURLOPT_RETURNTRANSFER => true, CURLOPT_ENCODING => '', CURLOPT_TIMEOUT => 10 * 60, // TODO Long 10 mins (probably shouldn't keep the request open...) diff --git a/app/Jobs/PlatformStatsSummaryJob.php b/app/Jobs/PlatformStatsSummaryJob.php index a8c5beeb..f201e0c2 100644 --- a/app/Jobs/PlatformStatsSummaryJob.php +++ b/app/Jobs/PlatformStatsSummaryJob.php @@ -80,7 +80,7 @@ public function prepareStats(array $allStats, $wikis): array { $currentTime = CarbonImmutable::now(); foreach ($wikis as $wiki) { - $this->apiUrl = $this->mwHostResolver->getBackendHostForDomain($wiki->domain) . '/w/api.php'; // used in PageFetcher::fetchPagesInNamespace + $this->apiUrl = $this->mwHostResolver->getBackendUrlForDomain($wiki->domain) . '/w/api.php'; // used in PageFetcher::fetchPagesInNamespace if (!is_null($wiki->deleted_at)) { $deletedWikis[] = $wiki; diff --git a/app/Jobs/PollForMediaWikiJobsJob.php b/app/Jobs/PollForMediaWikiJobsJob.php index 60d8d1e1..6e3ad016 100644 --- a/app/Jobs/PollForMediaWikiJobsJob.php +++ b/app/Jobs/PollForMediaWikiJobsJob.php @@ -28,7 +28,7 @@ private function hasPendingJobs(string $wikiDomain): bool { $response = Http::withHeaders([ 'host' => $wikiDomain, ])->get( - $this->mwHostResolver->getBackendHostForDomain($wikiDomain) . '/w/api.php?action=query&meta=siteinfo&siprop=statistics&format=json' + $this->mwHostResolver->getBackendUrlForDomain($wikiDomain) . '/w/api.php?action=query&meta=siteinfo&siprop=statistics&format=json' ); if ($response->failed()) { diff --git a/app/Jobs/SiteStatsUpdateJob.php b/app/Jobs/SiteStatsUpdateJob.php index d16afcdd..51c561fc 100644 --- a/app/Jobs/SiteStatsUpdateJob.php +++ b/app/Jobs/SiteStatsUpdateJob.php @@ -34,7 +34,7 @@ public function handle(HttpRequest $request, MediaWikiHostResolver $mwHostResolv Log::info(__METHOD__ . ": Updating stats for $wiki->domain"); $request->setOptions([ - CURLOPT_URL => $mwHostResolver->getBackendHostForDomain($wiki->domain) . '/w/api.php?action=wbstackSiteStatsUpdate&format=json', + CURLOPT_URL => $mwHostResolver->getBackendUrlForDomain($wiki->domain) . '/w/api.php?action=wbstackSiteStatsUpdate&format=json', CURLOPT_RETURNTRANSFER => true, CURLOPT_ENCODING => '', CURLOPT_TIMEOUT => 60 * 5, diff --git a/app/Jobs/UpdateWikiSiteStatsJob.php b/app/Jobs/UpdateWikiSiteStatsJob.php index a85aeb32..9abc1ee0 100644 --- a/app/Jobs/UpdateWikiSiteStatsJob.php +++ b/app/Jobs/UpdateWikiSiteStatsJob.php @@ -59,7 +59,7 @@ private function updateSiteStats(Wiki $wiki): void { $response = Http::withHeaders([ 'host' => $wiki->getAttribute('domain'), ])->get( - $this->mwHostResolver->getBackendHostForDomain($wiki->domain) . '/w/api.php?action=query&meta=siteinfo&siprop=statistics&format=json' + $this->mwHostResolver->getBackendUrlForDomain($wiki->domain) . '/w/api.php?action=query&meta=siteinfo&siprop=statistics&format=json' ); if ($response->failed()) { @@ -81,7 +81,7 @@ private function updateSiteStats(Wiki $wiki): void { private function getFirstEditedDate(Wiki $wiki): ?CarbonInterface { $allRevisions = Http::withHeaders(['host' => $wiki->getAttribute('domain')])->get( - $this->mwHostResolver->getBackendHostForDomain($wiki->domain) . '/w/api.php', + $this->mwHostResolver->getBackendUrlForDomain($wiki->domain) . '/w/api.php', [ 'action' => 'query', 'format' => 'json', @@ -99,7 +99,7 @@ private function getFirstEditedDate(Wiki $wiki): ?CarbonInterface { } $revisionInfo = Http::withHeaders(['host' => $wiki->getAttribute('domain')])->get( - $this->mwHostResolver->getBackendHostForDomain($wiki->domain) . '/w/api.php', + $this->mwHostResolver->getBackendUrlForDomain($wiki->domain) . '/w/api.php', [ 'action' => 'query', 'format' => 'json', @@ -119,7 +119,7 @@ private function getFirstEditedDate(Wiki $wiki): ?CarbonInterface { private function getLastEditedDate(Wiki $wiki): ?CarbonInterface { $allRevisions = Http::withHeaders(['host' => $wiki->getAttribute('domain')])->get( - $this->mwHostResolver->getBackendHostForDomain($wiki->domain) . '/w/api.php', + $this->mwHostResolver->getBackendUrlForDomain($wiki->domain) . '/w/api.php', [ 'action' => 'query', 'format' => 'json', @@ -137,7 +137,7 @@ private function getLastEditedDate(Wiki $wiki): ?CarbonInterface { } $revisionInfo = Http::withHeaders(['host' => $wiki->getAttribute('domain')])->get( - $this->mwHostResolver->getBackendHostForDomain($wiki->domain) . '/w/api.php', + $this->mwHostResolver->getBackendUrlForDomain($wiki->domain) . '/w/api.php', [ 'action' => 'query', 'format' => 'json', diff --git a/app/Jobs/WikiEntityImportJob.php b/app/Jobs/WikiEntityImportJob.php index aa069629..e8887426 100644 --- a/app/Jobs/WikiEntityImportJob.php +++ b/app/Jobs/WikiEntityImportJob.php @@ -77,7 +77,7 @@ private static function domainToOrigin(string $domain): string { private static function acquireCredentials(string $wikiDomain, MediaWikiHostResolver $mwHostResolver): OAuthCredentials { $response = Http::withHeaders(['host' => $wikiDomain])->asForm()->post( - $mwHostResolver->getBackendHostForDomain($wikiDomain) . '/w/api.php?action=wbstackPlatformOauthGet&format=json', + $mwHostResolver->getBackendUrlForDomain($wikiDomain) . '/w/api.php?action=wbstackPlatformOauthGet&format=json', [ 'consumerName' => 'WikiEntityImportJob', 'ownerOnly' => '1', From 38488e6d27cecda7c02e3bc11231c55c1c65b346 Mon Sep 17 00:00:00 2001 From: Dat Date: Fri, 26 Jun 2026 12:43:34 +0200 Subject: [PATCH 6/8] Correct method calls and proper mocking with Http:fake() --- tests/Commands/RebuildQueryserviceDataTest.php | 4 ++-- tests/Jobs/CirrusSearch/ForceSearchIndexTest.php | 4 ++-- .../Jobs/CirrusSearch/QueueSearchIndexBatchesTest.php | 4 ++-- tests/Jobs/MediawikiInitTest.php | 4 ++-- tests/Jobs/PlatformStatsSummaryJobTest.php | 4 ++-- tests/Jobs/PollForMediaWikiJobsJobTest.php | 4 ++-- tests/Jobs/SiteStatsUpdateJobTest.php | 4 ++-- tests/Jobs/UpdateWikiSiteStatsJobTest.php | 4 ++-- tests/Jobs/WikiEntityImportJobTest.php | 4 ++-- tests/Services/MediaWikiHostResolverTest.php | 11 +++++------ 10 files changed, 23 insertions(+), 24 deletions(-) diff --git a/tests/Commands/RebuildQueryserviceDataTest.php b/tests/Commands/RebuildQueryserviceDataTest.php index 07387a22..c7143762 100644 --- a/tests/Commands/RebuildQueryserviceDataTest.php +++ b/tests/Commands/RebuildQueryserviceDataTest.php @@ -25,10 +25,10 @@ protected function setUp(): void { WikiSetting::query()->delete(); QueryserviceNamespace::query()->delete(); - $this->mwBackendHost = 'mediawiki.localhost'; + $this->mwBackendHost = 'http://mediawiki.localhost'; $mockMwHostResolver = $this->createMock(MediaWikiHostResolver::class); - $mockMwHostResolver->method('getBackendHostForDomain')->willReturn( + $mockMwHostResolver->method('getBackendUrlForDomain')->willReturn( $this->mwBackendHost ); diff --git a/tests/Jobs/CirrusSearch/ForceSearchIndexTest.php b/tests/Jobs/CirrusSearch/ForceSearchIndexTest.php index c20b36cd..85d4899d 100644 --- a/tests/Jobs/CirrusSearch/ForceSearchIndexTest.php +++ b/tests/Jobs/CirrusSearch/ForceSearchIndexTest.php @@ -48,10 +48,10 @@ protected function setUp(): void { 'wiki_id' => $this->wiki->id, ]); - $this->mwBackendHost = 'mediawiki.localhost'; + $this->mwBackendHost = 'http://mediawiki.localhost'; $this->mockMwHostResolver = $this->createMock(MediaWikiHostResolver::class); - $this->mockMwHostResolver->method('getBackendHostForDomain')->willReturn( + $this->mockMwHostResolver->method('getBackendUrlForDomain')->willReturn( $this->mwBackendHost ); } diff --git a/tests/Jobs/CirrusSearch/QueueSearchIndexBatchesTest.php b/tests/Jobs/CirrusSearch/QueueSearchIndexBatchesTest.php index 1e7845c7..f005b50c 100644 --- a/tests/Jobs/CirrusSearch/QueueSearchIndexBatchesTest.php +++ b/tests/Jobs/CirrusSearch/QueueSearchIndexBatchesTest.php @@ -47,10 +47,10 @@ protected function setUp(): void { 'wiki_id' => $this->wiki->id, ]); - $this->mwBackendHost = 'mediawiki.localhost'; + $this->mwBackendHost = 'http://mediawiki.localhost'; $this->mockMwHostResolver = $this->createMock(MediaWikiHostResolver::class); - $this->mockMwHostResolver->method('getBackendHostForDomain')->willReturn( + $this->mockMwHostResolver->method('getBackendUrlForDomain')->willReturn( $this->mwBackendHost ); } diff --git a/tests/Jobs/MediawikiInitTest.php b/tests/Jobs/MediawikiInitTest.php index 0bd26a25..0cc8d139 100644 --- a/tests/Jobs/MediawikiInitTest.php +++ b/tests/Jobs/MediawikiInitTest.php @@ -24,10 +24,10 @@ protected function setUp(): void { $this->wikiDomain = 'some.domain.com'; $this->username = 'username'; $this->email = 'some@email.com'; - $this->mwBackendHost = 'mediawiki.localhost'; + $this->mwBackendHost = 'http://mediawiki.localhost'; $this->mockMwHostResolver = $this->createMock(MediaWikiHostResolver::class); - $this->mockMwHostResolver->method('getBackendHostForDomain')->willReturn( + $this->mockMwHostResolver->method('getBackendUrlForDomain')->willReturn( $this->mwBackendHost ); } diff --git a/tests/Jobs/PlatformStatsSummaryJobTest.php b/tests/Jobs/PlatformStatsSummaryJobTest.php index 31c0d4a2..f7284eb5 100644 --- a/tests/Jobs/PlatformStatsSummaryJobTest.php +++ b/tests/Jobs/PlatformStatsSummaryJobTest.php @@ -44,10 +44,10 @@ protected function setUp(): void { $this->wikis = []; $this->users = []; - $this->mwBackendHost = 'mediawiki.localhost'; + $this->mwBackendHost = 'http://mediawiki.localhost'; $this->mockMwHostResolver = $this->createMock(MediaWikiHostResolver::class); - $this->mockMwHostResolver->method('getBackendHostForDomain')->willReturn( + $this->mockMwHostResolver->method('getBackendUrlForDomain')->willReturn( $this->mwBackendHost ); } diff --git a/tests/Jobs/PollForMediaWikiJobsJobTest.php b/tests/Jobs/PollForMediaWikiJobsJobTest.php index e61d06f9..4dd7821c 100644 --- a/tests/Jobs/PollForMediaWikiJobsJobTest.php +++ b/tests/Jobs/PollForMediaWikiJobsJobTest.php @@ -26,10 +26,10 @@ protected function setUp(): void { parent::setUp(); $this->wiki = Wiki::factory()->create(); - $this->mwBackendHost = 'mediawiki.localhost'; + $this->mwBackendHost = 'http://mediawiki.localhost'; $this->mockMwHostResolver = $this->createMock(MediaWikiHostResolver::class); - $this->mockMwHostResolver->method('getBackendHostForDomain')->willReturn( + $this->mockMwHostResolver->method('getBackendUrlForDomain')->willReturn( $this->mwBackendHost ); } diff --git a/tests/Jobs/SiteStatsUpdateJobTest.php b/tests/Jobs/SiteStatsUpdateJobTest.php index 2851b4d1..77e03570 100644 --- a/tests/Jobs/SiteStatsUpdateJobTest.php +++ b/tests/Jobs/SiteStatsUpdateJobTest.php @@ -30,10 +30,10 @@ protected function setUp(): void { $this->user = User::factory()->create(['verified' => true]); $this->wiki = Wiki::factory()->create(); $this->manager = WikiManager::factory()->create(['wiki_id' => $this->wiki->id, 'user_id' => $this->user->id]); - $this->mwBackendHost = 'mediawiki.localhost'; + $this->mwBackendHost = 'http://mediawiki.localhost'; $this->mockMwHostResolver = $this->createMock(MediaWikiHostResolver::class); - $this->mockMwHostResolver->method('getBackendHostForDomain')->willReturn( + $this->mockMwHostResolver->method('getBackendUrlForDomain')->willReturn( $this->mwBackendHost ); } diff --git a/tests/Jobs/UpdateWikiSiteStatsJobTest.php b/tests/Jobs/UpdateWikiSiteStatsJobTest.php index 071cc880..5ea24625 100644 --- a/tests/Jobs/UpdateWikiSiteStatsJobTest.php +++ b/tests/Jobs/UpdateWikiSiteStatsJobTest.php @@ -27,10 +27,10 @@ protected function setUp(): void { $this->fakeResponses = []; Http::preventStrayRequests(); - $this->mwBackendHost = 'mediawiki.localhost'; + $this->mwBackendHost = 'http://mediawiki.localhost'; $this->mockMwHostResolver = $this->createMock(MediaWikiHostResolver::class); - $this->mockMwHostResolver->method('getBackendHostForDomain')->willReturn( + $this->mockMwHostResolver->method('getBackendUrlForDomain')->willReturn( $this->mwBackendHost ); } diff --git a/tests/Jobs/WikiEntityImportJobTest.php b/tests/Jobs/WikiEntityImportJobTest.php index 15c8aa6b..3a4bc983 100644 --- a/tests/Jobs/WikiEntityImportJobTest.php +++ b/tests/Jobs/WikiEntityImportJobTest.php @@ -29,10 +29,10 @@ protected function setUp(): void { Wiki::query()->delete(); WikiEntityImport::query()->delete(); - $this->mwBackendHost = 'mediawiki.localhost'; + $this->mwBackendHost = 'http://mediawiki.localhost'; $this->mockMwHostResolver = $this->createMock(MediaWikiHostResolver::class); - $this->mockMwHostResolver->method('getBackendHostForDomain')->willReturn( + $this->mockMwHostResolver->method('getBackendUrlForDomain')->willReturn( $this->mwBackendHost ); } diff --git a/tests/Services/MediaWikiHostResolverTest.php b/tests/Services/MediaWikiHostResolverTest.php index 77d993bb..daf18409 100644 --- a/tests/Services/MediaWikiHostResolverTest.php +++ b/tests/Services/MediaWikiHostResolverTest.php @@ -32,7 +32,7 @@ public function testResolverBuildsBackendUrl(): void { $resolver = new MediaWikiHostResolver(); $this->assertEquals( 'http://mediawiki-143-app-backend.default.svc.cluster.local', - $resolver->getBackendHostForDomain($domain) + $resolver->getBackendUrlForDomain($domain) ); } @@ -78,13 +78,12 @@ public function testGuzzleRejectsSchemelesUrls(): void { ); } - public function testGuzzleAcceptsSchemeUrls(): void { + public function testLaravelHttpAcceptsSchemeUrls(): void { Http::fake(['*' => Http::response()]); - $client = new Client(); - // This should NOT throw - $response = $client->get('http://mediawiki-143-app-backend.default.svc.cluster.local/w/api.php'); + // Using Laravel Http facade (which properly respects Http::fake()) + $response = Http::get('http://mediawiki-143-app-backend.default.svc.cluster.local/w/api.php'); - $this->assertTrue($response->getStatusCode() === 200); + $this->assertTrue($response->status() === 200); } } From 8a3ddc678a12c126fc87a64cd6dda03c090c949d Mon Sep 17 00:00:00 2001 From: Dat Date: Mon, 29 Jun 2026 17:35:15 +0200 Subject: [PATCH 7/8] Update to guzzle 7.13 --- composer.json | 2 +- composer.lock | 46 +++++++++++++++++++++++----------------------- 2 files changed, 24 insertions(+), 24 deletions(-) diff --git a/composer.json b/composer.json index 188d1f36..36943758 100644 --- a/composer.json +++ b/composer.json @@ -11,7 +11,7 @@ "absszero/laravel-stackdriver-error-reporting": "^1.9", "firebase/php-jwt": "^7.0", "google/recaptcha": "^1.2", - "guzzlehttp/guzzle": "^7.12", + "guzzlehttp/guzzle": "^7.13", "guzzlehttp/psr7": "^2.9", "hackzilla/password-generator": "^1.6", "intervention/image": "^2.5", diff --git a/composer.lock b/composer.lock index 2965d747..cc178315 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "518d96c6fc47a13344b85147f2f93526", + "content-hash": "4f87450f1479cff4b4f12411ac2cb301", "packages": [ { "name": "absszero/laravel-stackdriver-error-reporting", @@ -1691,26 +1691,26 @@ }, { "name": "guzzlehttp/guzzle", - "version": "7.12.0", + "version": "7.13.0", "source": { "type": "git", "url": "https://github.com/guzzle/guzzle.git", - "reference": "eaa81598031cf57a9e36258c8546defffc994cba" + "reference": "a4decaa9745dc567467970e43f183e260cfa51fd" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/guzzle/guzzle/zipball/eaa81598031cf57a9e36258c8546defffc994cba", - "reference": "eaa81598031cf57a9e36258c8546defffc994cba", + "url": "https://api.github.com/repos/guzzle/guzzle/zipball/a4decaa9745dc567467970e43f183e260cfa51fd", + "reference": "a4decaa9745dc567467970e43f183e260cfa51fd", "shasum": "" }, "require": { "ext-json": "*", "guzzlehttp/promises": "^2.5", - "guzzlehttp/psr7": "^2.12", + "guzzlehttp/psr7": "^2.12.3", "php": "^7.2.5 || ^8.0", "psr/http-client": "^1.0", "symfony/deprecation-contracts": "^2.5 || ^3.0", - "symfony/polyfill-php80": "^1.24" + "symfony/polyfill-php80": "^1.25" }, "provide": { "psr/http-client-implementation": "1.0" @@ -1719,7 +1719,7 @@ "bamarni/composer-bin-plugin": "^1.8.2", "ext-curl": "*", "guzzle/client-integration-tests": "3.0.2", - "guzzlehttp/test-server": "^0.5", + "guzzlehttp/test-server": "^0.6", "php-http/message-factory": "^1.1", "phpunit/phpunit": "^8.5.52 || ^9.6.34", "psr/log": "^1.1 || ^2.0 || ^3.0" @@ -1799,7 +1799,7 @@ ], "support": { "issues": "https://github.com/guzzle/guzzle/issues", - "source": "https://github.com/guzzle/guzzle/tree/7.12.0" + "source": "https://github.com/guzzle/guzzle/tree/7.13.0" }, "funding": [ { @@ -1815,7 +1815,7 @@ "type": "tidelift" } ], - "time": "2026-06-16T22:11:48+00:00" + "time": "2026-06-29T13:31:06+00:00" }, { "name": "guzzlehttp/promises", @@ -1903,16 +1903,16 @@ }, { "name": "guzzlehttp/psr7", - "version": "2.12.1", + "version": "2.12.3", "source": { "type": "git", "url": "https://github.com/guzzle/psr7.git", - "reference": "172ef2f4e9824c1e058b7f30be8ae25a02c0f2b7" + "reference": "7ec62dc3f44aa218487dbed81a9bf9bc647be55d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/guzzle/psr7/zipball/172ef2f4e9824c1e058b7f30be8ae25a02c0f2b7", - "reference": "172ef2f4e9824c1e058b7f30be8ae25a02c0f2b7", + "url": "https://api.github.com/repos/guzzle/psr7/zipball/7ec62dc3f44aa218487dbed81a9bf9bc647be55d", + "reference": "7ec62dc3f44aa218487dbed81a9bf9bc647be55d", "shasum": "" }, "require": { @@ -1921,7 +1921,7 @@ "psr/http-message": "^1.1 || ^2.0", "ralouphie/getallheaders": "^3.0", "symfony/deprecation-contracts": "^2.5 || ^3.0", - "symfony/polyfill-php80": "^1.24" + "symfony/polyfill-php80": "^1.25" }, "provide": { "psr/http-factory-implementation": "1.0", @@ -2002,7 +2002,7 @@ ], "support": { "issues": "https://github.com/guzzle/psr7/issues", - "source": "https://github.com/guzzle/psr7/tree/2.12.1" + "source": "https://github.com/guzzle/psr7/tree/2.12.3" }, "funding": [ { @@ -2018,7 +2018,7 @@ "type": "tidelift" } ], - "time": "2026-06-18T09:49:37+00:00" + "time": "2026-06-23T15:21:08+00:00" }, { "name": "guzzlehttp/uri-template", @@ -7145,16 +7145,16 @@ }, { "name": "symfony/deprecation-contracts", - "version": "v3.7.0", + "version": "v3.7.1", "source": { "type": "git", "url": "https://github.com/symfony/deprecation-contracts.git", - "reference": "50f59d1f3ca46d41ac911f97a78626b6756af35b" + "reference": "f3202fa1b5097b0af062dc978b32ecf63404e31d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/deprecation-contracts/zipball/50f59d1f3ca46d41ac911f97a78626b6756af35b", - "reference": "50f59d1f3ca46d41ac911f97a78626b6756af35b", + "url": "https://api.github.com/repos/symfony/deprecation-contracts/zipball/f3202fa1b5097b0af062dc978b32ecf63404e31d", + "reference": "f3202fa1b5097b0af062dc978b32ecf63404e31d", "shasum": "" }, "require": { @@ -7192,7 +7192,7 @@ "description": "A generic function and convention to trigger deprecation notices", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/deprecation-contracts/tree/v3.7.0" + "source": "https://github.com/symfony/deprecation-contracts/tree/v3.7.1" }, "funding": [ { @@ -7212,7 +7212,7 @@ "type": "tidelift" } ], - "time": "2026-04-13T15:52:40+00:00" + "time": "2026-06-05T06:23:12+00:00" }, { "name": "symfony/error-handler", From ac2c9bb8773f954ce200695d8a32c38694d41a6d Mon Sep 17 00:00:00 2001 From: Dat Date: Mon, 29 Jun 2026 17:43:44 +0200 Subject: [PATCH 8/8] Remove testGuzzleRejectsSchemelesUrls test --- tests/Services/MediaWikiHostResolverTest.php | 13 ------------- 1 file changed, 13 deletions(-) diff --git a/tests/Services/MediaWikiHostResolverTest.php b/tests/Services/MediaWikiHostResolverTest.php index daf18409..a7b6b372 100644 --- a/tests/Services/MediaWikiHostResolverTest.php +++ b/tests/Services/MediaWikiHostResolverTest.php @@ -8,8 +8,6 @@ use App\Wiki; use App\WikiDb; use Faker\Factory; -use GuzzleHttp\Client; -use GuzzleHttp\Exception\RequestException; use Illuminate\Foundation\Testing\RefreshDatabase; use Illuminate\Support\Facades\Http; @@ -67,17 +65,6 @@ public function testResolverThrowsIfUnableToFindWiki(): void { ); } - public function testGuzzleRejectsSchemelesUrls(): void { - $client = new Client(); - - // This should throw RequestException about empty scheme - $this->assertThrows( - fn () => $client->get('mediawiki-143-app-backend.default.svc.cluster.local/w/api.php'), - RequestException::class, - 'scheme "" is not allowed' - ); - } - public function testLaravelHttpAcceptsSchemeUrls(): void { Http::fake(['*' => Http::response()]);