feat: FontInstaller – Schrift-Upload über FontsUtility::font_add im Prozess (Manifest, Mirror font-install, Signatur-Referenzen)
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 5.5
parent
775d611cfa
commit
ad76d5ae66
@@ -0,0 +1,139 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace LIT\MCP\Integrations\Divi\Design;
|
||||
|
||||
use ET\Builder\VisualBuilder\Fonts\FontsUtility;
|
||||
use ET\Builder\VisualBuilder\Hooks\HooksRegistration;
|
||||
use LIT\MCP\Integrations\Divi\Release\Call;
|
||||
use LIT\MCP\Integrations\Divi\Release\DiviAdapter;
|
||||
use LIT\MCP\Integrations\Divi\Release\DiviFeature;
|
||||
use LIT\MCP\Integrations\Divi\Release\DiviLocked;
|
||||
use LIT\MCP\Integrations\Divi\Release\Manifest;
|
||||
use LIT\MCP\Integrations\Divi\Release\Mirror;
|
||||
use LIT\MCP\Integrations\Divi\Release\ReleaseGate;
|
||||
use RuntimeException;
|
||||
|
||||
defined('ABSPATH') || exit;
|
||||
|
||||
/**
|
||||
* Adapter "Designsystem" (spec §3) for uploaded fonts (ruling W8): installs server-side temp files through
|
||||
* FontsUtility::font_add() in-process with the upload action wp_handle_upload_rest – the way Divi's own AI agent
|
||||
* does it (AiAgentImageUtility::install_to_custom_fonts(), mirrored as calling convention) – after checking every
|
||||
* file with Divi's content check (check_filetype_and_ext_font). font_add() reports a partial success as success;
|
||||
* install() therefore returns the formats Divi really stored, the caller compares and removes the font again.
|
||||
* Not final so the upload tool can be tested with a fake.
|
||||
*/
|
||||
class FontInstaller implements DiviAdapter
|
||||
{
|
||||
/** Formats the plugin allows (ruling W8, no eot), in the order the tool lists them. */
|
||||
public const FORMATS = ['woff2', 'woff', 'ttf', 'otf'];
|
||||
|
||||
private const UNCHECKED = ['ext' => 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<string> 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<string, string> $files Extension => temp file path (deleted or moved by WordPress).
|
||||
* @return list<string> 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;
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
];
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -0,0 +1,248 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace LIT\MCP\Tests\Unit\Integrations\Divi\Design;
|
||||
|
||||
use Brain\Monkey\Functions;
|
||||
use LIT\MCP\Integrations\Divi\Design\FontInstaller;
|
||||
use LIT\MCP\Integrations\Divi\Release\Call;
|
||||
use LIT\MCP\Integrations\Divi\Release\ConstantRequirement;
|
||||
use LIT\MCP\Integrations\Divi\Release\DiviAdapters;
|
||||
use LIT\MCP\Integrations\Divi\Release\DiviFeature;
|
||||
use LIT\MCP\Integrations\Divi\Release\DiviLocked;
|
||||
use LIT\MCP\Integrations\Divi\Release\Manifest;
|
||||
use LIT\MCP\Integrations\Divi\Release\ReleaseGate;
|
||||
use LIT\MCP\Integrations\Divi\Release\ReleaseState;
|
||||
use LIT\MCP\Integrations\Divi\Release\ReleaseStatus;
|
||||
use LIT\MCP\Tests\Support\ReleaseMatrices;
|
||||
use LIT\MCP\Tests\Unit\TestCase;
|
||||
use Mockery;
|
||||
use RuntimeException;
|
||||
use WP_Error;
|
||||
|
||||
/**
|
||||
* Divi's FontsUtility and HooksRegistration are replaced by Mockery aliases; the tests that need them run in their
|
||||
* own process (an alias defines the class for the rest of the process).
|
||||
*/
|
||||
final class FontInstallerTest extends TestCase
|
||||
{
|
||||
private const FONTS = 'ET\\Builder\\VisualBuilder\\Fonts\\FontsUtility';
|
||||
private const HOOKS = 'ET\\Builder\\VisualBuilder\\Hooks\\HooksRegistration';
|
||||
private const UNCHECKED = ['ext' => false, 'type' => false, 'proper_filename' => false];
|
||||
|
||||
/** @var list<string> */
|
||||
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');
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user