diff --git a/src/Integrations/Divi/Design/FontInstaller.php b/src/Integrations/Divi/Design/FontInstaller.php new file mode 100644 index 0000000..2ff4f20 --- /dev/null +++ b/src/Integrations/Divi/Design/FontInstaller.php @@ -0,0 +1,139 @@ + false, 'type' => false, 'proper_filename' => false]; + + public static function manifest(): Manifest + { + return new Manifest( + Manifest::DESIGN, + calls: [ + new Call(FontsUtility::class . '::mime_types_font', 0), + new Call(HooksRegistration::class . '::check_filetype_and_ext_font', 3), + new Call(FontsUtility::class . '::font_add', 4), + new Call(FontsUtility::class . '::font_remove', 1), + ], + constants: [FontsUtility::class . '::FONT_NAME_MAX_LENGTH'], + mirrors: [ + new Mirror( + 'font-install', + 'includes/builder-5/server/VisualBuilder/AiAgent/AiAgentImageUtility.php', + 'install_to_custom_fonts', + 'AiAgentImageUtility', + 'Aufrufkonvention von font_add() mit serverseitiger Temp-Datei (check_filetype_and_ext_font ' + . 'vorab, wp_tempnam, action wp_handle_upload_rest)' + ), + ], + features: [DiviFeature::FontUpload] + ); + } + + /** @return list FORMATS that Divi's font MIME list knows too. */ + public function formats(): array + { + return array_values(array_intersect(self::FORMATS, array_keys(FontsUtility::mime_types_font()))); + } + + /** Divi's bound for a font name (FontNameRules::check() takes it). */ + public function nameMaxLength(): int + { + return (int) FontsUtility::FONT_NAME_MAX_LENGTH; + } + + /** Whether Divi's content check (finfo) takes $path as a font of $filename's extension. */ + public function accepts(string $path, string $filename): bool + { + return $this->mimeOf($path, $filename) !== null; + } + + /** + * @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. + */ + public function install(string $name, array $files, string $weights, string $family): array + { + ReleaseGate::assertFeature(DiviFeature::FontUpload); + $uploads = []; + foreach ($files as $extension => $path) { + $filename = sanitize_file_name($name . '.' . $extension); + $mime = $this->mimeOf($path, $filename); + if ($mime === null) { + throw new RuntimeException(sprintf("font file %s refused by Divi's type check", $extension)); + } + clearstatcache(true, $path); + $uploads[$extension] = [ + 'name' => $filename, + 'type' => $mime, + 'tmp_name' => $path, + 'error' => 0, + 'size' => (int) filesize($path), + ]; + } + + $result = FontsUtility::font_add( + $uploads, + $name, + ['font_weights' => $weights, 'generic_family' => $family], + ['action' => 'wp_handle_upload_rest'] + ); + if (is_wp_error($result)) { + throw new RuntimeException('font_add refused: ' . $result->get_error_message()); + } + + $stored = is_array($result) ? ($result['updated_fonts'][$name]['font_file'] ?? null) : null; + + return is_array($stored) ? array_map('strval', array_keys($stored)) : []; + } + + /** @throws RuntimeException When Divi does not remove the font (e.g. it does not exist). */ + public function remove(string $name): void + { + $result = FontsUtility::font_remove($name); + if (is_wp_error($result)) { + throw new RuntimeException('font_remove refused: ' . $result->get_error_message()); + } + } + + /** The MIME type Divi detected, or null when it refuses the content or names another extension. */ + private function mimeOf(string $path, string $filename): ?string + { + $checked = HooksRegistration::check_filetype_and_ext_font(self::UNCHECKED, $path, $filename); + $extension = strtolower((string) ($checked['ext'] ?? '')); + $type = (string) ($checked['type'] ?? ''); + + return $type !== '' && $extension !== '' && $extension === strtolower(pathinfo($filename, PATHINFO_EXTENSION)) + ? $type + : null; + } +} diff --git a/src/Integrations/Divi/Release/DiviAdapters.php b/src/Integrations/Divi/Release/DiviAdapters.php index 293e76f..d827d51 100644 --- a/src/Integrations/Divi/Release/DiviAdapters.php +++ b/src/Integrations/Divi/Release/DiviAdapters.php @@ -7,6 +7,7 @@ namespace LIT\MCP\Integrations\Divi\Release; use LIT\MCP\Integrations\Divi\Design\DesignSystemReader; use LIT\MCP\Integrations\Divi\Design\DesignSystemWriter; use LIT\MCP\Integrations\Divi\Design\FontCatalog; +use LIT\MCP\Integrations\Divi\Design\FontInstaller; use LIT\MCP\Integrations\Divi\Design\GlobalColorsAdapter; use LIT\MCP\Integrations\Divi\Design\GlobalVariablesAdapter; use LIT\MCP\Integrations\Divi\Design\ThemeOptionsAdapter; @@ -35,6 +36,7 @@ final class DiviAdapters GlobalVariablesAdapter::class, ThemeOptionsAdapter::class, FontCatalog::class, + FontInstaller::class, DiviModules::class, DynamicContentMap::class, ]; diff --git a/src/Integrations/Divi/Release/fingerprints.json b/src/Integrations/Divi/Release/fingerprints.json index 67bf1c9..3b8c553 100644 --- a/src/Integrations/Divi/Release/fingerprints.json +++ b/src/Integrations/Divi/Release/fingerprints.json @@ -1,5 +1,6 @@ { "customizer-typography": "0264fa5a2ba604542e1e1ad0c0339f04c8f4f67297087a0f92954ee63d343fd0", + "font-install": "a6b7c0f26437f9ea63186138e130f3c226c0cc017d424807e1030b44c165ea88", "library-submit-layout": "594b3843d4c42e1a91e5eda06f108c79583a0036e46c92dd749f4f541e362d02", "preset-sync": "9586f8005b8065893c0350977a58ee9171bf715fff21ac0c84663cc512f45522", "sig:ET\\Builder\\Framework\\UserRole\\UserRole::can_current_user_use_visual_builder": "76d199c13adc6600c8a456225a8eb9db8037064f20eca9db597c4e45ccecf8d5", @@ -22,6 +23,10 @@ "sig:ET\\Builder\\Packages\\ModuleLibrary\\ModuleRegistration::get_default_attrs": "728c9cddae0184ffccaff5b22a392b7a4e9c5f078b5d12d4c9717aca6af70e69", "sig:ET\\Builder\\Packages\\ModuleLibrary\\ModuleRegistration::get_module_settings": "f61b3693b0df17be1f068b8be424dcf56a67324372a92f576724cd564d992036", "sig:ET\\Builder\\Packages\\Module\\Layout\\Components\\DynamicContent\\DynamicContentOptions::get_options": "6d3932cbf7921be4f353ab71d2bd7200c2bf500577b401b32e9ca31809f07489", + "sig:ET\\Builder\\VisualBuilder\\Fonts\\FontsUtility::font_add": "028c32cd79e1f33dceebf23c4c08d39c7b8062da01fb3006318ce21597d55103", + "sig:ET\\Builder\\VisualBuilder\\Fonts\\FontsUtility::font_remove": "ab573a0776e9ba79e9606d3df9b1d2adb77e00e6eaf4120710a7649bcaf68ba0", + "sig:ET\\Builder\\VisualBuilder\\Fonts\\FontsUtility::mime_types_font": "dfbf01f6750e3fa6c309f1200effcfda539d75c3404732cd5df939a8bff399ae", + "sig:ET\\Builder\\VisualBuilder\\Hooks\\HooksRegistration::check_filetype_and_ext_font": "d98fd02bab7ba06af60ac0a99fbc717f916dd126d19c8f2e69779ab83d99902f", "sig:ET_Core_PageResource::remove_static_resources": "eaa39fd2c037c2d399578f71862da5c04b68d58a30657916482f95e94e5d8a96", "sig:et_": "ae7b0ec701e388b03d29d972195cb727fe1c8fd75dba4657ad71f1a963a32966", "sig:et_builder_enabled_for_post_type": "a6aed8043b2c07ef1082488fde706816cf0146847f5bac8acca79fb3fe5275fc", diff --git a/tests/Fixtures/divi/reference/font-install.txt b/tests/Fixtures/divi/reference/font-install.txt new file mode 100644 index 0000000..7852a32 --- /dev/null +++ b/tests/Fixtures/divi/reference/font-install.txt @@ -0,0 +1,148 @@ +function install_to_custom_fonts( string $chat_id, string $filename, string $font_name = '' ) { + if ( ! extension_loaded( 'fileinfo' ) ) { + return new WP_Error( + 'fileinfo_extension_not_loaded', + esc_html__( 'Font install requires the fileinfo PHP extension.', 'et_builder_5' ), + [ 'status' => 400 ] + ); + } + + if ( ! self::_validate_chat_id( $chat_id ) ) { + return new WP_Error( + 'invalid_chat_id', + esc_html__( 'Invalid chat identifier.', 'et_builder_5' ), + [ 'status' => 400 ] + ); + } + + $ownership = self::assert_chat_ownership( $chat_id ); + + if ( is_wp_error( $ownership ) ) { + return $ownership; + } + + if ( false !== strpos( $filename, '/' ) || false !== strpos( $filename, '\\' ) ) { + return new WP_Error( + 'invalid_url', + esc_html__( 'Could not parse chat ID and filename from URL.', 'et_builder_5' ), + [ 'status' => 400 ] + ); + } + + $filename = sanitize_file_name( $filename ); + $base_dir = realpath( self::_get_base_dir() ); + + if ( false === $base_dir || '' === $filename ) { + return new WP_Error( + 'invalid_file', + esc_html__( 'Invalid file reference.', 'et_builder_5' ), + [ 'status' => 400 ] + ); + } + + $source_path = self::_get_base_dir() . '/' . $chat_id . '/' . $filename; + $resolved = realpath( $source_path ); + + if ( false === $resolved || ! str_starts_with( $resolved, $base_dir ) || ! file_exists( $resolved ) ) { + return new WP_Error( + 'file_not_found', + esc_html__( 'Attachment file not found.', 'et_builder_5' ), + [ 'status' => 404 ] + ); + } + + $file_size = filesize( $resolved ); + + if ( false === $file_size || self::MAX_FILE_SIZE < $file_size ) { + return new WP_Error( + 'file_too_large', + esc_html__( 'Font must be under 10MB.', 'et_builder_5' ), + [ 'status' => 400 ] + ); + } + + $ext = strtolower( pathinfo( $filename, PATHINFO_EXTENSION ) ); + $supported = self::_get_supported_font_formats(); + + if ( '' === $ext || ! in_array( $ext, $supported, true ) ) { + return new WP_Error( + 'invalid_font_format', + esc_html__( 'This font format is not supported.', 'et_builder_5' ), + [ 'status' => 400 ] + ); + } + + $checked = HooksRegistration::check_filetype_and_ext_font( + [ + 'ext' => false, + 'type' => false, + 'proper_filename' => false, + ], + $resolved, + $filename + ); + + if ( empty( $checked['ext'] ) || empty( $checked['type'] ) ) { + return new WP_Error( + 'invalid_mime', + esc_html__( 'Only supported font files can be installed as custom fonts.', 'et_builder_5' ), + [ 'status' => 400 ] + ); + } + + if ( ! in_array( strtolower( (string) $checked['ext'] ), $supported, true ) ) { + return new WP_Error( + 'invalid_font_format', + esc_html__( 'This font format is not supported.', 'et_builder_5' ), + [ 'status' => 400 ] + ); + } + + $temp = wp_tempnam( $filename ); + + if ( false === $temp ) { + return new WP_Error( + 'temp_file_error', + esc_html__( 'Failed to prepare font file for install.', 'et_builder_5' ), + [ 'status' => 500 ] + ); + } + + if ( ! copy( $resolved, $temp ) ) { + wp_delete_file( $temp ); + + return new WP_Error( + 'copy_failed', + esc_html__( 'Failed to prepare font file for install.', 'et_builder_5' ), + [ 'status' => 500 ] + ); + } + + $resolved_font_name = '' !== $font_name ? $font_name : pathinfo( $filename, PATHINFO_FILENAME ); + + $result = FontsUtility::font_add( + [ + $checked['ext'] => [ + 'name' => $filename, + 'type' => $checked['type'], + 'tmp_name' => $temp, + 'error' => 0, + 'size' => filesize( $temp ), + ], + ], + $resolved_font_name, + [ + 'font_weights' => 'all', + 'generic_family' => 'sans-serif', + ], + [ + 'action' => 'wp_handle_upload_rest', + ] + ); + + if ( file_exists( $temp ) ) { + wp_delete_file( $temp ); + } + + return $result; + } diff --git a/tests/Unit/Integrations/Divi/Cli/DiviCheckCommandTest.php b/tests/Unit/Integrations/Divi/Cli/DiviCheckCommandTest.php index 6c914bd..ba18abc 100644 --- a/tests/Unit/Integrations/Divi/Cli/DiviCheckCommandTest.php +++ b/tests/Unit/Integrations/Divi/Cli/DiviCheckCommandTest.php @@ -228,14 +228,15 @@ final class DiviCheckCommandTest extends TestCase self::assertSame(0, $code); self::assertSame( [ - 'customizer-typography', 'library-submit-layout', 'preset-sync', 'sig:A::b', 'sig:et_pb_is_allowed', - 'tb-api-save', 'tb-load-setting-options', 'tb-store-template', 'vb-sync-flags', + 'customizer-typography', 'font-install', 'library-submit-layout', 'preset-sync', 'sig:A::b', + 'sig:et_pb_is_allowed', 'tb-api-save', 'tb-load-setting-options', 'tb-store-template', + 'vb-sync-flags', ], array_keys(DiviReference::expected($this->expectedFile)) ); self::assertSame(str_repeat('b', 64), DiviReference::expected($this->expectedFile)['sig:A::b']); self::assertContains( - 'Referenz aktualisiert (7 Fingerabdrücke, 2 Signaturen). Smoke-Skripte laufen lassen, dann committen.', + 'Referenz aktualisiert (8 Fingerabdrücke, 2 Signaturen). Smoke-Skripte laufen lassen, dann committen.', $this->lines ); self::assertStringStartsWith( @@ -244,7 +245,7 @@ final class DiviCheckCommandTest extends TestCase ); } - /** Minimal stand-ins for the seven mirrored Divi functions at their real paths. */ + /** Minimal stand-ins for the eight mirrored Divi functions at their real paths. */ private function writeDivi(): void { $files = [ @@ -258,6 +259,8 @@ final class DiviCheckCommandTest extends TestCase "class SyncToServerController {\n\tpublic static function update( \$request ) {\n\t}\n}", 'includes/builder-5/server/Packages/GlobalData/GlobalPresetController.php' => "class GlobalPresetController {\n\tpublic static function sync( \$request ) {\n\t}\n}", + 'includes/builder-5/server/VisualBuilder/AiAgent/AiAgentImageUtility.php' => + "class AiAgentImageUtility {\n\tpublic static function install_to_custom_fonts( \$a, \$b ) {\n\t}\n}", ]; foreach ($files as $path => $code) { $file = $this->themeDir . '/' . $path; diff --git a/tests/Unit/Integrations/Divi/Design/FontInstallerTest.php b/tests/Unit/Integrations/Divi/Design/FontInstallerTest.php new file mode 100644 index 0000000..6738666 --- /dev/null +++ b/tests/Unit/Integrations/Divi/Design/FontInstallerTest.php @@ -0,0 +1,248 @@ + false, 'type' => false, 'proper_filename' => false]; + + /** @var list */ + private array $files = []; + + protected function setUp(): void + { + parent::setUp(); + Functions\when('sanitize_file_name')->alias(static fn (string $name): string => str_replace(' ', '-', $name)); + } + + protected function tearDown(): void + { + foreach ($this->files as $file) { + if (is_file($file)) { + unlink($file); + } + } + parent::tearDown(); + } + + private function tempFile(string $bytes): string + { + $file = (string) tempnam(sys_get_temp_dir(), 'lit-font'); + file_put_contents($file, $bytes); + $this->files[] = $file; + + return $file; + } + + public function testIsARegisteredDesignAdapterForFontUpload(): void + { + $manifest = FontInstaller::manifest(); + + self::assertContains(FontInstaller::class, DiviAdapters::CLASSES); + self::assertSame(Manifest::DESIGN, $manifest->area); + self::assertSame( + ['FontsUtility::mime_types_font()', 'HooksRegistration::check_filetype_and_ext_font()', + 'FontsUtility::font_add()', 'FontsUtility::font_remove()'], + array_map(static fn (Call $call): string => $call->label(), $manifest->calls) + ); + self::assertSame([0, 3, 4, 1], array_map(static fn (Call $call): ?int => $call->args, $manifest->calls)); + self::assertSame( + [self::FONTS . '::FONT_NAME_MAX_LENGTH'], + array_map(static fn (ConstantRequirement $constant): string => $constant->name, $manifest->constants) + ); + self::assertCount(1, $manifest->mirrors); + self::assertSame('font-install', $manifest->mirrors[0]->id); + self::assertSame( + 'includes/builder-5/server/VisualBuilder/AiAgent/AiAgentImageUtility.php', + $manifest->mirrors[0]->file + ); + self::assertSame('AiAgentImageUtility::install_to_custom_fonts()', $manifest->mirrors[0]->label()); + self::assertSame([DiviFeature::FontUpload], $manifest->features); + } + + public function testInstallNeedsAReleasedDivi(): void + { + ReleaseGate::useResolver( + static fn (): ReleaseStatus => ReleaseMatrices::uniform(ReleaseState::Untested, '5.16.0') + ); + + $this->expectException(DiviLocked::class); + + (new FontInstaller())->install('Inter', ['woff2' => '/tmp/x'], 'all', 'sans-serif'); + } + + /** @runInSeparateProcess */ + public function testFormatsAreThePluginsFourAsFarAsDiviKnowsThem(): void + { + Mockery::mock('alias:' . self::FONTS)->allows('mime_types_font')->andReturn([ + 'otf' => ['font/otf'], + 'ttf' => ['font/ttf'], + 'woff' => ['font/woff'], + 'eot' => ['application/vnd.ms-fontobject'], + ]); + + self::assertSame(['woff', 'ttf', 'otf'], (new FontInstaller())->formats(), 'no woff2 in Divi, never eot'); + } + + /** @runInSeparateProcess */ + public function testTheNameLimitIsDivis(): void + { + Mockery::getConfiguration()->setConstantsMap([self::FONTS => ['FONT_NAME_MAX_LENGTH' => 80]]); + Mockery::mock('alias:' . self::FONTS); + + self::assertSame(80, (new FontInstaller())->nameMaxLength()); + } + + /** @runInSeparateProcess */ + public function testAcceptsAsksDivisContentCheckAndWantsTheGivenExtensionBack(): void + { + $file = $this->tempFile('wOF2DATA'); + $hooks = Mockery::mock('alias:' . self::HOOKS); + $hooks->expects('check_filetype_and_ext_font')->with(self::UNCHECKED, $file, 'Inter.woff2') + ->andReturn(['ext' => 'woff2', 'type' => 'font/woff2', 'proper_filename' => 'Inter.woff2']); + $hooks->expects('check_filetype_and_ext_font')->with(self::UNCHECKED, $file, 'Inter.ttf') + ->andReturn(['ext' => 'woff2', 'type' => 'font/woff2', 'proper_filename' => 'Inter.ttf']); + $hooks->expects('check_filetype_and_ext_font')->with(self::UNCHECKED, $file, 'Inter.otf') + ->andReturn(self::UNCHECKED); + + $installer = new FontInstaller(); + + self::assertTrue($installer->accepts($file, 'Inter.woff2')); + self::assertFalse($installer->accepts($file, 'Inter.ttf'), 'Divi reports another extension'); + self::assertFalse($installer->accepts($file, 'Inter.otf'), 'Divi refuses the content'); + } + + /** @runInSeparateProcess */ + public function testInstallHandsTheTempFilesToFontAddLikeDivisAiAgent(): void + { + $woff2 = $this->tempFile('wOF2DATA'); + $ttf = $this->tempFile("\x00\x01\x00\x00TTF"); + $hooks = Mockery::mock('alias:' . self::HOOKS); + $hooks->allows('check_filetype_and_ext_font')->andReturnUsing( + static fn (array $unchecked, string $file, string $name): array => [ + 'ext' => pathinfo($name, PATHINFO_EXTENSION), + 'type' => 'font/' . pathinfo($name, PATHINFO_EXTENSION), + 'proper_filename' => $name, + ] + ); + Mockery::mock('alias:' . self::FONTS)->expects('font_add')->with( + [ + 'woff2' => ['name' => 'Inter-Display.woff2', 'type' => 'font/woff2', 'tmp_name' => $woff2, + 'error' => 0, 'size' => 8], + 'ttf' => ['name' => 'Inter-Display.ttf', 'type' => 'font/ttf', 'tmp_name' => $ttf, 'error' => 0, + 'size' => 7], + ], + 'Inter Display', + ['font_weights' => '400,700', 'generic_family' => 'serif'], + ['action' => 'wp_handle_upload_rest'] + )->andReturn([ + 'uploaded_font' => 'Inter Display', + 'updated_fonts' => [ + 'Andere' => ['font_file' => ['otf' => '/up/et-fonts/Andere.otf']], + 'Inter Display' => ['font_file' => [ + 'woff2' => '/up/et-fonts/Inter-Display.woff2', + 'ttf' => '/up/et-fonts/Inter-Display.ttf', + ]], + ], + ]); + + $installed = (new FontInstaller())->install( + 'Inter Display', + ['woff2' => $woff2, 'ttf' => $ttf], + '400,700', + 'serif' + ); + + self::assertSame(['woff2', 'ttf'], $installed); + } + + /** @runInSeparateProcess */ + public function testAPartialSuccessListsOnlyTheFormatsDiviStored(): void + { + $woff2 = $this->tempFile('wOF2DATA'); + $ttf = $this->tempFile("\x00\x01\x00\x00TTF"); + Mockery::mock('alias:' . self::HOOKS)->allows('check_filetype_and_ext_font')->andReturnUsing( + static fn (array $unchecked, string $file, string $name): array => [ + 'ext' => pathinfo($name, PATHINFO_EXTENSION), + 'type' => 'font/x', + 'proper_filename' => $name, + ] + ); + Mockery::mock('alias:' . self::FONTS)->allows('font_add')->andReturn([ + 'uploaded_font' => 'Inter', + 'updated_fonts' => ['Inter' => ['font_file' => ['woff2' => '/up/et-fonts/Inter.woff2']]], + ]); + + self::assertSame( + ['woff2'], + (new FontInstaller())->install('Inter', ['woff2' => $woff2, 'ttf' => $ttf], 'all', 'sans-serif') + ); + } + + /** @runInSeparateProcess */ + public function testAFileDivisTypeCheckRefusesStopsTheInstallBeforeFontAdd(): void + { + $woff2 = $this->tempFile('wOF2DATA'); + Mockery::mock('alias:' . self::HOOKS)->allows('check_filetype_and_ext_font')->andReturn(self::UNCHECKED); + Mockery::mock('alias:' . self::FONTS)->expects('font_add')->never(); + + $this->expectException(RuntimeException::class); + $this->expectExceptionMessage("font file woff2 refused by Divi's type check"); + + (new FontInstaller())->install('Inter', ['woff2' => $woff2], 'all', 'sans-serif'); + } + + /** @runInSeparateProcess */ + public function testARefusedFontAddIsARuntimeException(): void + { + $woff2 = $this->tempFile('wOF2DATA'); + 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)->allows('font_add') + ->andReturn(new WP_Error('upload_error_handler', 'Sorry, you are not allowed to upload this file type.')); + + $this->expectException(RuntimeException::class); + $this->expectExceptionMessage('font_add refused: Sorry, you are not allowed to upload this file type.'); + + (new FontInstaller())->install('Inter', ['woff2' => $woff2], 'all', 'sans-serif'); + } + + /** @runInSeparateProcess */ + public function testRemoveDeletesTheFontAndReportsARefusal(): void + { + $fonts = Mockery::mock('alias:' . self::FONTS); + $fonts->expects('font_remove')->with('Inter')->andReturn(['updated_fonts' => []]); + $fonts->expects('font_remove')->with('Fehlt')->andReturn(new WP_Error('font_not_exist', 'Font Does not Exist.')); + + (new FontInstaller())->remove('Inter'); + + $this->expectException(RuntimeException::class); + $this->expectExceptionMessage('font_remove refused: Font Does not Exist.'); + (new FontInstaller())->remove('Fehlt'); + } +} diff --git a/tests/Unit/Integrations/Divi/Release/DiviFeatureAssignmentTest.php b/tests/Unit/Integrations/Divi/Release/DiviFeatureAssignmentTest.php index 1b32ee5..f0b62f9 100644 --- a/tests/Unit/Integrations/Divi/Release/DiviFeatureAssignmentTest.php +++ b/tests/Unit/Integrations/Divi/Release/DiviFeatureAssignmentTest.php @@ -7,6 +7,7 @@ namespace LIT\MCP\Tests\Unit\Integrations\Divi\Release; use LIT\MCP\Integrations\Divi\Design\DesignSystemReader; use LIT\MCP\Integrations\Divi\Design\DesignSystemWriter; use LIT\MCP\Integrations\Divi\Design\FontCatalog; +use LIT\MCP\Integrations\Divi\Design\FontInstaller; use LIT\MCP\Integrations\Divi\Design\GlobalColorsAdapter; use LIT\MCP\Integrations\Divi\Design\GlobalVariablesAdapter; use LIT\MCP\Integrations\Divi\Design\ThemeOptionsAdapter; @@ -104,6 +105,14 @@ final class DiviFeatureAssignmentTest extends TestCase 'et_builder_get_websafe_fonts()' => ['font-upload', 'theme-option-write', 'variable-write'], 'et_pb_is_allowed()' => ['font-upload'], ], + FontInstaller::class => [ + 'FontsUtility::mime_types_font()' => ['font-upload'], + 'HooksRegistration::check_filetype_and_ext_font()' => ['font-upload'], + 'FontsUtility::font_add()' => ['font-upload'], + 'FontsUtility::font_remove()' => ['font-upload'], + 'Konstante ET\\Builder\\VisualBuilder\\Fonts\\FontsUtility::FONT_NAME_MAX_LENGTH' => ['font-upload'], + 'Nachbau AiAgentImageUtility::install_to_custom_fonts()' => ['font-upload'], + ], DiviModules::class => [ 'ModuleRegistration::get_all_core_modules_metadata()' => ['catalog'], 'ModuleRegistration::get_default_attrs()' => ['catalog'], @@ -165,7 +174,7 @@ final class DiviFeatureAssignmentTest extends TestCase } self::assertSame(self::EXPECTED, $actual); - self::assertSame(array_keys(self::EXPECTED), DiviAdapters::CLASSES, 'all twelve adapters, in report order'); + self::assertSame(array_keys(self::EXPECTED), DiviAdapters::CLASSES, 'all thirteen adapters, in report order'); } public function testReadingAdaptersCarryNoWriteFeature(): void