From 2b45f1bdd35d0667879c925b22b1dfc113c395c0 Mon Sep 17 00:00:00 2001 From: Jan-Niclas Loosen Date: Mon, 19 Oct 2026 18:27:02 +0200 Subject: [PATCH] =?UTF-8?q?fix:=20FontInstaller=20pr=C3=BCft=20Divis=20Sch?= =?UTF-8?q?riftenliste=20vor=20font=5Fadd;=20leere=20Installation=20ist=20?= =?UTF-8?q?ein=20sauberer=20Rollback?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Sonnet 5.5 --- .../Divi/Design/DiviFontsUploadAbility.php | 7 +++++++ src/Integrations/Divi/Design/FontInstaller.php | 10 +++++++++- .../Divi/Design/DiviFontsUploadAbilityTest.php | 18 ++++++++++++++++++ .../Divi/Design/FontInstallerTest.php | 17 +++++++++++++++++ 4 files changed, 51 insertions(+), 1 deletion(-) diff --git a/src/Integrations/Divi/Design/DiviFontsUploadAbility.php b/src/Integrations/Divi/Design/DiviFontsUploadAbility.php index baef4fa..2d7a512 100644 --- a/src/Integrations/Divi/Design/DiviFontsUploadAbility.php +++ b/src/Integrations/Divi/Design/DiviFontsUploadAbility.php @@ -411,6 +411,13 @@ final class DiviFontsUploadAbility implements Ability, RequiresDiviFeatures throw WriteFailed::rolledBack(self::REASON, $e, 'font upload: ' . $e->getMessage()); } + if ($installed === []) { + throw WriteFailed::rolledBack( + self::REASON . ' Divi hat nichts gespeichert.', + detail: 'font upload: Divi stored nothing of ' . implode(',', array_keys($paths)) + ); + } + $missing = array_values(array_diff(array_keys($paths), $installed)); if ($missing === []) { return; diff --git a/src/Integrations/Divi/Design/FontInstaller.php b/src/Integrations/Divi/Design/FontInstaller.php index 2ff4f20..8a99a45 100644 --- a/src/Integrations/Divi/Design/FontInstaller.php +++ b/src/Integrations/Divi/Design/FontInstaller.php @@ -43,6 +43,8 @@ class FontInstaller implements DiviAdapter new Call(FontsUtility::class . '::font_remove', 1), ], constants: [FontsUtility::class . '::FONT_NAME_MAX_LENGTH'], + // Read before font_add(): a corrupt list would make Divi lose the files it moved. + keys: ['et_uploaded_fonts'], mirrors: [ new Mirror( 'font-install', @@ -79,11 +81,17 @@ class FontInstaller implements DiviAdapter * @param array $files Extension => temp file path (deleted or moved by WordPress). * @return list The extensions Divi stored – fewer than $files on a partial success. * @throws DiviLocked When font-upload is not released – before anything is written. - * @throws RuntimeException When Divi refuses a file or the whole font; nothing is stored then. + * @throws RuntimeException When Divi's font list is corrupt or Divi refuses a file or the whole font; nothing is stored then. */ public function install(string $name, array $files, string $weights, string $family): array { ReleaseGate::assertFeature(DiviFeature::FontUpload); + if (! is_array(get_option('et_uploaded_fonts', []))) { + throw new RuntimeException( + 'Divis Schriftenliste (et_uploaded_fonts) ist beschädigt; nichts wurde gespeichert. ' + . 'Die Option in Divi bzw. der Datenbank prüfen.' + ); + } $uploads = []; foreach ($files as $extension => $path) { $filename = sanitize_file_name($name . '.' . $extension); diff --git a/tests/Unit/Integrations/Divi/Design/DiviFontsUploadAbilityTest.php b/tests/Unit/Integrations/Divi/Design/DiviFontsUploadAbilityTest.php index d058d02..b41e02a 100644 --- a/tests/Unit/Integrations/Divi/Design/DiviFontsUploadAbilityTest.php +++ b/tests/Unit/Integrations/Divi/Design/DiviFontsUploadAbilityTest.php @@ -464,6 +464,24 @@ final class DiviFontsUploadAbilityTest extends TestCase $this->assertTempFilesRemoved(); } + public function testAnInstallThatStoredNothingIsACleanRollback(): void + { + $installer = self::installer(); + $installer->allows('install')->andReturn([]); + $installer->expects('remove')->never(); + $log = FakeChangeLog::partialsOnly(); + + try { + $this->ability($installer, $log)->execute(self::input(['confirm' => true])); + self::fail('WriteFailed expected'); + } catch (WriteFailed $e) { + self::assertTrue($e->rolledBack); + $error = self::assertToolError($e->toError(), 'write_failed'); + self::assertStringContainsString('nichts gespeichert', $error['message']); + } + self::assertSame([], $log->partials); + } + public function testAPartialInstallThatCannotBeRemovedIsAPartialWrite(): void { $installer = self::installer(); diff --git a/tests/Unit/Integrations/Divi/Design/FontInstallerTest.php b/tests/Unit/Integrations/Divi/Design/FontInstallerTest.php index 6738666..e59b616 100644 --- a/tests/Unit/Integrations/Divi/Design/FontInstallerTest.php +++ b/tests/Unit/Integrations/Divi/Design/FontInstallerTest.php @@ -37,6 +37,7 @@ final class FontInstallerTest extends TestCase protected function setUp(): void { parent::setUp(); + Functions\when('get_option')->justReturn([]); Functions\when('sanitize_file_name')->alias(static fn (string $name): string => str_replace(' ', '-', $name)); } @@ -83,6 +84,7 @@ final class FontInstallerTest extends TestCase ); self::assertSame('AiAgentImageUtility::install_to_custom_fonts()', $manifest->mirrors[0]->label()); self::assertSame([DiviFeature::FontUpload], $manifest->features); + self::assertSame(['et_uploaded_fonts'], $manifest->keys); } public function testInstallNeedsAReleasedDivi(): void @@ -204,6 +206,21 @@ final class FontInstallerTest extends TestCase ); } + /** @runInSeparateProcess */ + public function testACorruptFontListStopsTheInstallBeforeFontAdd(): void + { + $woff2 = $this->tempFile('wOF2DATA'); + Functions\when('get_option')->justReturn('kaputt'); + Mockery::mock('alias:' . self::HOOKS)->allows('check_filetype_and_ext_font') + ->andReturn(['ext' => 'woff2', 'type' => 'font/woff2', 'proper_filename' => 'Inter.woff2']); + Mockery::mock('alias:' . self::FONTS)->expects('font_add')->never(); + + $this->expectException(RuntimeException::class); + $this->expectExceptionMessage('et_uploaded_fonts'); + + (new FontInstaller())->install('Inter', ['woff2' => $woff2], 'all', 'sans-serif'); + } + /** @runInSeparateProcess */ public function testAFileDivisTypeCheckRefusesStopsTheInstallBeforeFontAdd(): void {