feat: FontSignature (Magic Bytes) und FontNameRules (Namensregeln eigener Schriften)

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
Jan-Niclas Loosen
2026-10-19 18:02:29 +02:00
co-authored by Claude Sonnet 5.5
parent 172a703903
commit 775d611cfa
4 changed files with 201 additions and 0 deletions
@@ -0,0 +1,74 @@
<?php
declare(strict_types=1);
namespace LIT\MCP\Integrations\Divi\Design;
defined('ABSPATH') || exit;
/**
* Name rules of an uploaded font (ruling W9), stricter than FontsUtility::font_add(): Divi silently strips every
* character but A–Z, a–z, 0–9, whitespace, _ and -; the plugin refuses them and names what Divi would store.
* Divi compares names with Google's and uploaded fonts case-sensitively and skips the web-safe ones; the plugin
* refuses any of them whatever the case, so a font never shadows another one in Divi's font lists. Pure PHP.
*/
final class FontNameRules
{
/** = FontsUtility::FONT_NAME_MAX_LENGTH in Divi 5.15.1 (FontInstaller::nameMaxLength() reads Divi's). */
public const MAX_LENGTH = 100;
private const ALLOWED = '/^[A-Za-z0-9 _-]+$/D';
/** What FontsUtility::font_add() removes from a name. */
private const DIVI_STRIPS = '/[^A-Za-z0-9\s\_-]/';
/**
* @param list<string> $reserved Names of Google, web-safe and uploaded fonts (FontCatalog::reserved()).
* @return string|null null = valid, else the problem (German).
*/
public static function check(string $name, array $reserved, int $maxLength = self::MAX_LENGTH): ?string
{
if (trim($name) === '') {
return sprintf('Name fehlt (1–%d Zeichen)', $maxLength);
}
if (preg_match(self::ALLOWED, $name) !== 1) {
$stored = trim((string) preg_replace(self::DIVI_STRIPS, '', $name));
return 'nur Buchstaben A–Z/a–z, Ziffern, Leerzeichen, _ und - erlaubt ('
. ($stored === '' ? 'Divi ließe nichts übrig)' : sprintf('Divi würde „%s“ speichern)', $stored));
}
if ($name !== trim($name)) {
return 'ohne Leerzeichen am Anfang oder Ende';
}
if (strlen($name) > $maxLength) {
return sprintf('höchstens %d Zeichen (%d angegeben)', $maxLength, strlen($name));
}
$taken = self::taken($name, $reserved);
return $taken === null ? null : sprintf(
'„%s“ gibt es schon als Google-, Web-Safe- oder hochgeladene Schrift (ohne Beachtung der '
. 'Groß-/Kleinschreibung)',
$taken
);
}
/**
* @param list<string> $reserved
* @return string|null The existing name $name collides with (case-insensitive), null when it is free.
*/
public static function taken(string $name, array $reserved): ?string
{
$lower = strtolower($name);
foreach ($reserved as $existing) {
if (strtolower($existing) === $lower) {
return $existing;
}
}
return null;
}
}
@@ -0,0 +1,32 @@
<?php
declare(strict_types=1);
namespace LIT\MCP\Integrations\Divi\Design;
defined('ABSPATH') || exit;
/**
* The font format of a file by its magic bytes (ruling W9): wOF2 = woff2, wOFF = woff, 00 01 00 00 or "true" =
* ttf, OTTO = otf. Divi's own check (finfo) may be skipped for users with unfiltered_upload, so the font upload
* checks the content itself. Font collections (ttcf) and eot are no font here. Pure PHP.
*/
final class FontSignature
{
/** Bytes detect() needs from the start of the file. */
public const HEADER_BYTES = 4;
private const MAGIC = [
'wOF2' => 'woff2',
'wOFF' => 'woff',
"\x00\x01\x00\x00" => 'ttf',
'true' => 'ttf',
'OTTO' => 'otf',
];
/** @return 'woff2'|'woff'|'ttf'|'otf'|null */
public static function detect(string $header): ?string
{
return self::MAGIC[substr($header, 0, self::HEADER_BYTES)] ?? null;
}
}