Skip to content

Commit 3cfafae

Browse files
committed
Fix old 3.3.x boards using bad composer package files
1 parent bee1a7a commit 3cfafae

4 files changed

Lines changed: 209 additions & 0 deletions

File tree

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
<?php
2+
/**
3+
*
4+
* QuickInstall CLI
5+
*
6+
* @copyright (c) 2026 phpBB Limited <https://www.phpbb.com>
7+
* @license GNU General Public License, version 2 (GPL-2.0)
8+
*
9+
*/
10+
11+
namespace QuickInstall\Sandbox;
12+
13+
use RuntimeException;
14+
15+
/** Normalizes Composer metadata required by legacy phpBB dependencies. */
16+
class ComposerMetadataCompatibility
17+
{
18+
/**
19+
* Restores the flat installed.json format expected by package-versions 1.x.
20+
*
21+
* Composer 2 uses installed.php itself, so retaining the legacy JSON shape is
22+
* safe until Composer next regenerates the vendor metadata.
23+
*/
24+
public static function normalizePackageVersions(string $root): bool
25+
{
26+
$root = rtrim(str_replace('\\', '/', $root), '/') . '/';
27+
$versionsPath = $root . 'vendor/ocramius/package-versions/src/PackageVersions/Versions.php';
28+
$installedPath = $root . 'vendor/composer/installed.json';
29+
if (!is_file($versionsPath) || !is_file($installedPath))
30+
{
31+
return false;
32+
}
33+
34+
$versions = file_get_contents($versionsPath);
35+
if (!is_string($versions) || !preg_match('/const\s+VERSIONS\s*=\s*\[\s*\]\s*;/', $versions))
36+
{
37+
return false;
38+
}
39+
40+
$installed = file_get_contents($installedPath);
41+
$data = json_decode((string) $installed, true);
42+
if (!is_array($data))
43+
{
44+
throw new RuntimeException("Invalid legacy Composer metadata: $installedPath");
45+
}
46+
if (!isset($data['packages']))
47+
{
48+
if (is_string($installed) && substr(ltrim($installed), 0, 1) === '[')
49+
{
50+
// Composer 1 metadata already uses the format expected by the fallback.
51+
return false;
52+
}
53+
54+
throw new RuntimeException("Unsupported Composer metadata format: $installedPath");
55+
}
56+
if (!is_array($data['packages']))
57+
{
58+
throw new RuntimeException("Invalid Composer package metadata: $installedPath");
59+
}
60+
61+
$json = json_encode($data['packages'], JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES);
62+
if ($json === false)
63+
{
64+
throw new RuntimeException("Unable to encode legacy Composer metadata: $installedPath");
65+
}
66+
67+
$contents = $json . "\n";
68+
if (file_put_contents($installedPath, $contents, LOCK_EX) !== strlen($contents))
69+
{
70+
throw new RuntimeException("Unable to normalize legacy Composer metadata: $installedPath");
71+
}
72+
73+
return true;
74+
}
75+
}

src/QuickInstall/Sandbox/SourceProvider.php

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -359,6 +359,7 @@ public function fetch(array $source): void
359359

360360
$this->normalizeGitSourceRoot($path);
361361
$this->run($this->composerCommand(['install', '--no-interaction', '--ignore-platform-reqs']), $path);
362+
ComposerMetadataCompatibility::normalizePackageVersions($path);
362363
return;
363364
}
364365

@@ -385,6 +386,7 @@ public function fetch(array $source): void
385386
}
386387

387388
$this->run($command, dirname($path));
389+
ComposerMetadataCompatibility::normalizePackageVersions($path);
388390
}
389391

390392
protected function normalizeGitSourceRoot(string $path): void
@@ -470,6 +472,7 @@ protected function installedPhpbbVersion(string $path): string
470472

471473
protected function withInstalledSourceMetadata(array $source, ?string $defaultPhp): array
472474
{
475+
ComposerMetadataCompatibility::normalizePackageVersions($source['path'] ?? '');
473476
$detectedVersion = $this->detectedPhpbbVersion($source['path'] ?? '');
474477
if ($detectedVersion !== null)
475478
{

src/QuickInstall/Sandbox/bootstrap.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
require_once __DIR__ . '/DoctorService.php';
2020
require_once __DIR__ . '/VersionMatrix.php';
2121
require_once __DIR__ . '/UpdateService.php';
22+
require_once __DIR__ . '/ComposerMetadataCompatibility.php';
2223
require_once __DIR__ . '/SourceProvider.php';
2324
require_once __DIR__ . '/SourceService.php';
2425
require_once __DIR__ . '/BoardService.php';

tests/Unit/SourceProviderTest.php

Lines changed: 130 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,23 @@ public function testFetchComposerBuildsCreateProjectCommand(): void
106106
self::assertSame(dirname($project->sourcePath('3.3.14')), $provider->runs[0]['cwd']);
107107
}
108108

109+
public function testFetchComposerNormalizesLegacyPackageVersionsMetadata(): void
110+
{
111+
$project = $this->project();
112+
$provider = new CompatibilitySourceProvider($project);
113+
$path = $project->sourcePath('3.3.2');
114+
115+
$provider->fetch([
116+
'type' => 'composer',
117+
'constraint' => '3.3.2',
118+
'path' => $path,
119+
]);
120+
121+
self::assertSame([
122+
['name' => 'ocramius/proxy-manager', 'version' => '2.1.1'],
123+
], json_decode((string) file_get_contents($path . '/vendor/composer/installed.json'), true));
124+
}
125+
109126
public function testFetchGitNormalizesPhpbbSubdirectoryAndRunsComposerInstall(): void
110127
{
111128
$project = $this->project();
@@ -126,6 +143,66 @@ public function testFetchGitNormalizesPhpbbSubdirectoryAndRunsComposerInstall():
126143
self::assertSame(['composer-bin', 'install', '--no-interaction', '--ignore-platform-reqs'], $provider->runs[1]['command']);
127144
}
128145

146+
public function testFetchGitNormalizesLegacyPackageVersionsMetadata(): void
147+
{
148+
$project = $this->project();
149+
$provider = new CompatibilitySourceProvider($project);
150+
$path = $project->sourcePath('custom');
151+
152+
$provider->fetch([
153+
'type' => 'git',
154+
'url' => 'https://github.com/phpbb/phpbb.git',
155+
'branch' => 'release-3.3.2',
156+
'version' => 'release-3.3.2',
157+
'path' => $path,
158+
]);
159+
160+
self::assertSame([
161+
['name' => 'ocramius/proxy-manager', 'version' => '2.1.1'],
162+
], json_decode((string) file_get_contents($path . '/vendor/composer/installed.json'), true));
163+
}
164+
165+
public function testEnsureNormalizesReusedComposerSource(): void
166+
{
167+
$project = $this->project();
168+
$this->addDownloadedSource($project, '3.3.2');
169+
$path = $project->sourcePath('3.3.2');
170+
$this->addLegacyComposerMetadata($path);
171+
172+
(new TestSourceProvider($project))->ensure('3.3.2');
173+
174+
self::assertSame([
175+
['name' => 'ocramius/proxy-manager', 'version' => '2.1.1'],
176+
], json_decode((string) file_get_contents($path . '/vendor/composer/installed.json'), true));
177+
}
178+
179+
public function testEnsureLeavesGeneratedPackageVersionsMetadataUntouched(): void
180+
{
181+
$project = $this->project();
182+
$this->addDownloadedSource($project, '3.3.2');
183+
$path = $project->sourcePath('3.3.2');
184+
$this->addLegacyComposerMetadata($path, false);
185+
$installedPath = $path . '/vendor/composer/installed.json';
186+
$metadata = file_get_contents($installedPath);
187+
188+
(new TestSourceProvider($project))->ensure('3.3.2');
189+
190+
self::assertSame($metadata, file_get_contents($installedPath));
191+
}
192+
193+
public function testEnsureRejectsUnsupportedFallbackMetadata(): void
194+
{
195+
$project = $this->project();
196+
$this->addDownloadedSource($project, '3.3.2');
197+
$path = $project->sourcePath('3.3.2');
198+
$this->addLegacyComposerMetadata($path);
199+
file_put_contents($path . '/vendor/composer/installed.json', '{}');
200+
201+
$this->expectException(\RuntimeException::class);
202+
$this->expectExceptionMessage('Unsupported Composer metadata format');
203+
(new TestSourceProvider($project))->ensure('3.3.2');
204+
}
205+
129206
public function testAddGitSourceAcceptsCloneUrlEndingInGit(): void
130207
{
131208
$project = $this->project();
@@ -265,6 +342,22 @@ private function addDownloadedSource(Project $project, string $key, string $phpR
265342
];
266343
$project->writeJson('sources.json', $sources);
267344
}
345+
346+
private function addLegacyComposerMetadata(string $path, bool $fallback = true): void
347+
{
348+
$versionsDirectory = $path . '/vendor/ocramius/package-versions/src/PackageVersions';
349+
$composerDirectory = $path . '/vendor/composer';
350+
mkdir($versionsDirectory, 0775, true);
351+
mkdir($composerDirectory, 0775, true);
352+
$versions = $fallback ? '[]' : "['phpbb/phpbb' => '3.3.2']";
353+
file_put_contents($versionsDirectory . '/Versions.php', "<?php final class Versions { const VERSIONS = $versions; }");
354+
file_put_contents($composerDirectory . '/installed.json', json_encode([
355+
'packages' => [
356+
['name' => 'ocramius/proxy-manager', 'version' => '2.1.1'],
357+
],
358+
'dev' => true,
359+
]));
360+
}
268361
}
269362

270363
class TestSourceProvider extends SourceProvider
@@ -296,6 +389,43 @@ protected function capture(array $command, string $cwd): array
296389
}
297390
}
298391

392+
class CompatibilitySourceProvider extends TestSourceProvider
393+
{
394+
protected function run(array $command, string $cwd): void
395+
{
396+
parent::run($command, $cwd);
397+
if (($command[1] ?? '') === 'create-project')
398+
{
399+
$this->addLegacyComposerMetadata($command[3]);
400+
}
401+
else if (($command[1] ?? '') === 'install')
402+
{
403+
$this->addLegacyComposerMetadata($cwd);
404+
}
405+
}
406+
407+
private function addLegacyComposerMetadata(string $path): void
408+
{
409+
$versionsDirectory = $path . '/vendor/ocramius/package-versions/src/PackageVersions';
410+
$composerDirectory = $path . '/vendor/composer';
411+
if (!is_dir($versionsDirectory))
412+
{
413+
mkdir($versionsDirectory, 0775, true);
414+
}
415+
if (!is_dir($composerDirectory))
416+
{
417+
mkdir($composerDirectory, 0775, true);
418+
}
419+
file_put_contents($versionsDirectory . '/Versions.php', '<?php final class Versions { const VERSIONS = []; }');
420+
file_put_contents($composerDirectory . '/installed.json', json_encode([
421+
'packages' => [
422+
['name' => 'ocramius/proxy-manager', 'version' => '2.1.1'],
423+
],
424+
'dev' => true,
425+
]));
426+
}
427+
}
428+
299429
class RefreshingSourceProvider extends SourceProvider
300430
{
301431
private int $refresh = 0;

0 commit comments

Comments
 (0)