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:
Jan-Niclas Loosen
2026-10-19 18:03:48 +02:00
co-authored by Claude Sonnet 5.5
parent 775d611cfa
commit ad76d5ae66
7 changed files with 559 additions and 5 deletions
@@ -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",