feat: Fähigkeiten theme-option-read/-write und font-upload; ThemeOptionsAdapter und FontCatalog (Manifest, Nachbau customizer-typography, Signatur-Referenzen) und DesignRights::themeOptions()/fonts()
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 5.5
parent
8379c55c89
commit
a90cd7f5bd
@@ -13,11 +13,15 @@ defined('ABSPATH') || exit;
|
|||||||
/**
|
/**
|
||||||
* Who may change the design system (spec §10): presets and colors need edit_theme_options and Divi's role right
|
* Who may change the design system (spec §10): presets and colors need edit_theme_options and Divi's role right
|
||||||
* "Visual Builder verwenden" – what Divi's own preset sync asks since 5.15 (formats "Nachtrag 5.15.1"); variables
|
* "Visual Builder verwenden" – what Divi's own preset sync asks since 5.15 (formats "Nachtrag 5.15.1"); variables
|
||||||
* also the role right "Variables Manager". Without unfiltered_html a write is refused while presets it saves along
|
* also the role right "Variables Manager". Theme options need edit_theme_options and the role right
|
||||||
* hold HTML/CSS that Divi would strip (final review I-2).
|
* "Theme-Anpassung" (what Divi's customizer asks), font uploads upload_files, edit_theme_options and the role right
|
||||||
|
* "Eigene Schriften verwalten" (Divi's font route plus the plugin's rule, plan 4d). Without unfiltered_html a write is
|
||||||
|
* refused while presets it saves along hold HTML/CSS that Divi would strip (final review I-2).
|
||||||
*/
|
*/
|
||||||
final class DesignRights
|
final class DesignRights
|
||||||
{
|
{
|
||||||
|
private const ROLE_EDITOR = 'Einen Administrator bitten, das Recht unter Divi → Rollen-Editor freizugeben.';
|
||||||
|
|
||||||
public const OTHER_MARKUP = 'Andere Presets enthalten HTML/CSS, das beim Speichern ohne das Recht unfiltered_html '
|
public const OTHER_MARKUP = 'Andere Presets enthalten HTML/CSS, das beim Speichern ohne das Recht unfiltered_html '
|
||||||
. 'verloren ginge; nichts wurde gespeichert.';
|
. 'verloren ginge; nichts wurde gespeichert.';
|
||||||
|
|
||||||
@@ -70,18 +74,52 @@ final class DesignRights
|
|||||||
return Errors::forbidden(
|
return Errors::forbidden(
|
||||||
'Die Divi-Rolle dieses Kontos erlaubt den Variablen-Manager nicht; ohne ihn speichert Divi keine '
|
'Die Divi-Rolle dieses Kontos erlaubt den Variablen-Manager nicht; ohne ihn speichert Divi keine '
|
||||||
. 'Variablen.',
|
. 'Variablen.',
|
||||||
'Einen Administrator bitten, das Recht unter Divi → Rollen-Editor freizugeben.'
|
self::ROLE_EDITOR
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Theme options (writing and restoring): edit_theme_options and Divi's role right "Theme-Anpassung". $writer is
|
||||||
|
* not asked – Divi's customizer needs no Visual Builder; it keeps the signature of the other design rights.
|
||||||
|
*/
|
||||||
|
public static function themeOptions(DesignSystemWriter $writer, ThemeOptionsAdapter $options): ?WP_Error
|
||||||
|
{
|
||||||
|
$refusal = self::themeRight('Theme-Optionen ändern');
|
||||||
|
if ($refusal !== null || $options->canCustomize()) {
|
||||||
|
return $refusal;
|
||||||
|
}
|
||||||
|
|
||||||
|
return Errors::forbidden(
|
||||||
|
'Die Divi-Rolle dieses Kontos erlaubt die Theme-Anpassung nicht; ohne sie ändert LIT MCP keine '
|
||||||
|
. 'Theme-Optionen.',
|
||||||
|
self::ROLE_EDITOR
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Font uploads: upload_files, edit_theme_options and Divi's role right "Eigene Schriften verwalten". */
|
||||||
|
public static function fonts(FontCatalog $fonts): ?WP_Error
|
||||||
|
{
|
||||||
|
if (! current_user_can('upload_files')) {
|
||||||
|
return Errors::forbidden('Schriften hochladen erfordert das Recht, Dateien hochzuladen (upload_files).');
|
||||||
|
}
|
||||||
|
$refusal = self::themeRight('Schriften hochladen');
|
||||||
|
if ($refusal !== null || $fonts->canManage()) {
|
||||||
|
return $refusal;
|
||||||
|
}
|
||||||
|
|
||||||
|
return Errors::forbidden(
|
||||||
|
'Die Divi-Rolle dieses Kontos erlaubt „Eigene Schriften verwalten“ nicht; ohne das Recht lädt LIT MCP '
|
||||||
|
. 'keine Schriften hoch.',
|
||||||
|
self::ROLE_EDITOR
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** edit_theme_options and Divi's role right "Visual Builder verwenden"; $what names the area in the message. */
|
/** edit_theme_options and Divi's role right "Visual Builder verwenden"; $what names the area in the message. */
|
||||||
private static function designer(DesignSystemWriter $writer, string $what): ?WP_Error
|
private static function designer(DesignSystemWriter $writer, string $what): ?WP_Error
|
||||||
{
|
{
|
||||||
if (! current_user_can('edit_theme_options')) {
|
$refusal = self::themeRight($what . ' ändern');
|
||||||
return Errors::forbidden(sprintf(
|
if ($refusal !== null) {
|
||||||
'%s ändern erfordert das Recht, das Design der Website anzupassen (edit_theme_options).',
|
return $refusal;
|
||||||
$what
|
|
||||||
));
|
|
||||||
}
|
}
|
||||||
if (! $writer->canUseVisualBuilder()) {
|
if (! $writer->canUseVisualBuilder()) {
|
||||||
return Errors::forbidden(
|
return Errors::forbidden(
|
||||||
@@ -89,10 +127,23 @@ final class DesignRights
|
|||||||
'Die Divi-Rolle dieses Kontos erlaubt den Visual Builder nicht; ohne ihn ändert LIT MCP keine %s.',
|
'Die Divi-Rolle dieses Kontos erlaubt den Visual Builder nicht; ohne ihn ändert LIT MCP keine %s.',
|
||||||
$what
|
$what
|
||||||
),
|
),
|
||||||
'Einen Administrator bitten, das Recht unter Divi → Rollen-Editor freizugeben.'
|
self::ROLE_EDITOR
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** edit_theme_options; $activity starts the message ("Presets ändern", "Schriften hochladen"). */
|
||||||
|
private static function themeRight(string $activity): ?WP_Error
|
||||||
|
{
|
||||||
|
if (current_user_can('edit_theme_options')) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return Errors::forbidden(sprintf(
|
||||||
|
'%s erfordert das Recht, das Design der Website anzupassen (edit_theme_options).',
|
||||||
|
$activity
|
||||||
|
));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,95 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace LIT\MCP\Integrations\Divi\Design;
|
||||||
|
|
||||||
|
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\Manifest;
|
||||||
|
|
||||||
|
defined('ABSPATH') || exit;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adapter "Designsystem" (spec §3) for Divi's font lists (plan 4d W5, W9): the families Divi's customizer sanitizer
|
||||||
|
* accepts (uploaded fonts + et_builder_get_fonts(), exact names), the uploaded ones, and every reserved name for an
|
||||||
|
* upload, plus Divi's role right "Eigene Schriften verwalten". et_builder_get_google_fonts() parses about 1.3 MB and
|
||||||
|
* may fetch from Google once a day, so names() is loaded once per instance – DiviIntegration builds one per request;
|
||||||
|
* uploaded() and reserved() are read fresh (an upload changes them). Not final so the tools can be tested with a mock.
|
||||||
|
*/
|
||||||
|
class FontCatalog implements DiviAdapter
|
||||||
|
{
|
||||||
|
/** @var list<string>|null */
|
||||||
|
private ?array $names = null;
|
||||||
|
|
||||||
|
public static function manifest(): Manifest
|
||||||
|
{
|
||||||
|
return new Manifest(
|
||||||
|
Manifest::DESIGN,
|
||||||
|
calls: [
|
||||||
|
new Call('et_builder_get_fonts', 0),
|
||||||
|
new Call('et_builder_get_custom_fonts', 0),
|
||||||
|
new Call('et_builder_get_google_fonts', 0),
|
||||||
|
new Call('et_builder_get_websafe_fonts', 0),
|
||||||
|
new Call('et_pb_is_allowed', 1, [DiviFeature::FontUpload]),
|
||||||
|
],
|
||||||
|
// The options behind Divi's lists; read only through the functions above.
|
||||||
|
keys: ['et_uploaded_fonts', 'et_google_fonts_cache', 'et_google_api_settings'],
|
||||||
|
// Plan 4d W1: font variables are checked against the same list, so a break locks all three on purpose.
|
||||||
|
features: [DiviFeature::ThemeOptionWrite, DiviFeature::FontUpload, DiviFeature::VariableWrite]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @return list<string> Exact family names Divi's sanitizer accepts: uploaded, then web-safe and Google. */
|
||||||
|
public function names(): array
|
||||||
|
{
|
||||||
|
return $this->names ??= self::namesOf(et_builder_get_custom_fonts(), et_builder_get_fonts());
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @return list<string> */
|
||||||
|
public function uploaded(): array
|
||||||
|
{
|
||||||
|
return self::namesOf(et_builder_get_custom_fonts());
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @return list<string> Google, web-safe and uploaded names – an upload must not reuse one (W9). */
|
||||||
|
public function reserved(): array
|
||||||
|
{
|
||||||
|
return self::namesOf(
|
||||||
|
et_builder_get_google_fonts(),
|
||||||
|
et_builder_get_websafe_fonts(),
|
||||||
|
et_builder_get_custom_fonts()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function has(string $name): bool
|
||||||
|
{
|
||||||
|
return in_array($name, $this->names(), true);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Divi's role right "Eigene Schriften verwalten" (custom_fonts_management); fail closed without Divi. */
|
||||||
|
public function canManage(): bool
|
||||||
|
{
|
||||||
|
return function_exists('et_pb_is_allowed') && (bool) et_pb_is_allowed('custom_fonts_management');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param mixed ...$lists Divi's lists (family => settings); anything else (a broken filter) is skipped.
|
||||||
|
* @return list<string>
|
||||||
|
*/
|
||||||
|
private static function namesOf(mixed ...$lists): array
|
||||||
|
{
|
||||||
|
$names = [];
|
||||||
|
foreach ($lists as $list) {
|
||||||
|
if (! is_array($list)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
foreach (array_keys($list) as $name) {
|
||||||
|
$names[] = (string) $name;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return array_values(array_unique($names));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,187 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace LIT\MCP\Integrations\Divi\Design;
|
||||||
|
|
||||||
|
use InvalidArgumentException;
|
||||||
|
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 the whitelisted theme options of Divi's customizer (plan 4d W2, W6): reads
|
||||||
|
* them raw from get_option('et_divi'), sends a value through the customizer's own sanitizer of its setting, and
|
||||||
|
* writes the way a customizer save ends – et_update_option()/et_delete_option() per key, then the static CSS and the
|
||||||
|
* builder caches (research A.2/A.3). Limits (W13): et_update_option() writes the whole et_divi array from Divi's
|
||||||
|
* request global, so a change another process made after this request loaded it is lost; design_rev and the lock of
|
||||||
|
* the pipeline narrow that window. remove_static_resources() does nothing without a logged-in user. Not final so the
|
||||||
|
* theme option area can be tested with a mock.
|
||||||
|
*/
|
||||||
|
class ThemeOptionsAdapter implements DiviAdapter
|
||||||
|
{
|
||||||
|
/** Divi's theme options (one array option). */
|
||||||
|
private const OPTIONS = 'et_divi';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The 20 whitelist keys (plan 4d W2, in this order) => the sanitize_callback of their setting in
|
||||||
|
* et_divi_customizer_theme_settings() (mirror customizer-typography). sanitize() calls it by this name; every
|
||||||
|
* Divi name here is a call of the manifest (ThemeOptionsAdapterTest), absint is WordPress core.
|
||||||
|
*/
|
||||||
|
public const SANITIZERS = [
|
||||||
|
'heading_font' => 'et_sanitize_font_choices',
|
||||||
|
'body_font' => 'et_sanitize_font_choices',
|
||||||
|
'primary_nav_font' => 'et_sanitize_font_choices',
|
||||||
|
'secondary_nav_font' => 'et_sanitize_font_choices',
|
||||||
|
'heading_font_weight' => 'et_sanitize_global_font_weight',
|
||||||
|
'body_font_weight' => 'et_sanitize_global_font_weight',
|
||||||
|
'body_font_size' => 'absint',
|
||||||
|
'tablet_body_font_size' => 'absint',
|
||||||
|
'phone_body_font_size' => 'absint',
|
||||||
|
'body_header_size' => 'absint',
|
||||||
|
'tablet_header_font_size' => 'absint',
|
||||||
|
'phone_header_font_size' => 'absint',
|
||||||
|
'body_font_height' => 'et_sanitize_float_number',
|
||||||
|
'body_header_height' => 'et_sanitize_float_number',
|
||||||
|
'body_header_spacing' => 'et_sanitize_int_number',
|
||||||
|
'body_header_style' => 'et_divi_global_font_style',
|
||||||
|
'content_width' => 'absint',
|
||||||
|
'gutter_width' => 'absint',
|
||||||
|
'section_padding' => 'absint',
|
||||||
|
'row_padding' => 'absint',
|
||||||
|
];
|
||||||
|
|
||||||
|
public static function manifest(): Manifest
|
||||||
|
{
|
||||||
|
return new Manifest(
|
||||||
|
Manifest::DESIGN,
|
||||||
|
calls: [
|
||||||
|
new Call('et_sanitize_font_choices', 1),
|
||||||
|
new Call('et_sanitize_global_font_weight', 1),
|
||||||
|
new Call('et_sanitize_float_number', 1),
|
||||||
|
new Call('et_sanitize_int_number', 1),
|
||||||
|
new Call('et_divi_global_font_style', 1),
|
||||||
|
new Call('et_update_option', 2),
|
||||||
|
new Call('et_delete_option', 1),
|
||||||
|
new Call('ET_Core_PageResource::remove_static_resources', 2),
|
||||||
|
new Call('et_pb_force_regenerate_templates', 0),
|
||||||
|
new Call('et_pb_is_allowed', 1),
|
||||||
|
],
|
||||||
|
keys: [self::OPTIONS],
|
||||||
|
mirrors: [
|
||||||
|
// shortcut: the fingerprint covers the whole customizer function (about 4,000 lines) and changes with
|
||||||
|
// every customizer edit; switch to a narrower excerpt when it fires too often.
|
||||||
|
new Mirror(
|
||||||
|
'customizer-typography',
|
||||||
|
'functions.php',
|
||||||
|
'et_divi_customizer_theme_settings',
|
||||||
|
null,
|
||||||
|
'Standards, Bereiche und Sanitizer der 20 Theme-Optionen (ThemeOptionSpec)',
|
||||||
|
[DiviFeature::ThemeOptionRead, DiviFeature::ThemeOptionWrite]
|
||||||
|
),
|
||||||
|
],
|
||||||
|
features: [DiviFeature::ThemeOptionWrite]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The whitelist keys present in et_divi with their raw value, in the order of SANITIZERS; a missing key (Divi's
|
||||||
|
* default) is missing here too. A value of another type (array, null, true) is no customizer value and left out.
|
||||||
|
*
|
||||||
|
* @return array<string, int|float|string|false>
|
||||||
|
*/
|
||||||
|
public function read(): array
|
||||||
|
{
|
||||||
|
$options = get_option(self::OPTIONS);
|
||||||
|
if (! is_array($options)) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
$stored = [];
|
||||||
|
foreach (array_keys(self::SANITIZERS) as $key) {
|
||||||
|
$value = $options[$key] ?? null;
|
||||||
|
if (is_int($value) || is_float($value) || is_string($value) || $value === false) {
|
||||||
|
$stored[$key] = $value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $stored;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Divi's customizer sanitizer of $key applied to $value; false = Divi refuses it (Divi itself would store false).
|
||||||
|
* Non-scalars never reach Divi's sanitizers.
|
||||||
|
*
|
||||||
|
* @throws InvalidArgumentException When $key is not a whitelist key.
|
||||||
|
*/
|
||||||
|
public function sanitize(string $key, mixed $value): mixed
|
||||||
|
{
|
||||||
|
$sanitizer = self::SANITIZERS[$key] ?? throw new InvalidArgumentException(
|
||||||
|
'not a theme option of the whitelist: ' . $key
|
||||||
|
);
|
||||||
|
|
||||||
|
return is_scalar($value) ? $sanitizer($value) : false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Writes like a customizer save: $set per key with et_update_option(), $unset (back to Divi's default) with
|
||||||
|
* et_delete_option(), then the static CSS and the builder caches; reads back from et_divi.
|
||||||
|
*
|
||||||
|
* @param array<string, int|float|string|false> $set Checked values (ThemeOptionSpec, sanitize()) or raw values
|
||||||
|
* of a snapshot.
|
||||||
|
* @param list<string> $unset
|
||||||
|
* @return array<string, int|float|string|false> read() after writing.
|
||||||
|
* @throws DiviLocked When theme-option-write is not released – before anything is written.
|
||||||
|
* @throws InvalidArgumentException When a key is not a whitelist key – before anything is written.
|
||||||
|
* @throws RuntimeException When the read-back differs from what was written.
|
||||||
|
*/
|
||||||
|
public function write(array $set, array $unset): array
|
||||||
|
{
|
||||||
|
ReleaseGate::assertFeature(DiviFeature::ThemeOptionWrite);
|
||||||
|
$unknown = array_diff([...array_keys($set), ...$unset], array_keys(self::SANITIZERS));
|
||||||
|
if ($unknown !== []) {
|
||||||
|
throw new InvalidArgumentException('not a theme option of the whitelist: ' . implode(', ', $unknown));
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($set as $key => $value) {
|
||||||
|
et_update_option($key, $value);
|
||||||
|
}
|
||||||
|
foreach ($unset as $key) {
|
||||||
|
et_delete_option($key);
|
||||||
|
}
|
||||||
|
// What Divi runs on customize_save_after (research A.2): the customizer CSS lives in the static resources.
|
||||||
|
\ET_Core_PageResource::remove_static_resources('all', 'all');
|
||||||
|
et_pb_force_regenerate_templates();
|
||||||
|
|
||||||
|
$stored = $this->read();
|
||||||
|
$wrong = [];
|
||||||
|
foreach ($set as $key => $value) {
|
||||||
|
if (! array_key_exists($key, $stored) || $stored[$key] !== $value) {
|
||||||
|
$wrong[] = $key;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
foreach ($unset as $key) {
|
||||||
|
if (array_key_exists($key, $stored)) {
|
||||||
|
$wrong[] = $key;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if ($wrong !== []) {
|
||||||
|
throw new RuntimeException('theme options not written: ' . implode(', ', $wrong));
|
||||||
|
}
|
||||||
|
|
||||||
|
return $stored;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Divi's role right "Theme-Anpassung" (theme_customizer); fail closed without Divi. */
|
||||||
|
public function canCustomize(): bool
|
||||||
|
{
|
||||||
|
return function_exists('et_pb_is_allowed') && (bool) et_pb_is_allowed('theme_customizer');
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -6,8 +6,10 @@ namespace LIT\MCP\Integrations\Divi\Release;
|
|||||||
|
|
||||||
use LIT\MCP\Integrations\Divi\Design\DesignSystemReader;
|
use LIT\MCP\Integrations\Divi\Design\DesignSystemReader;
|
||||||
use LIT\MCP\Integrations\Divi\Design\DesignSystemWriter;
|
use LIT\MCP\Integrations\Divi\Design\DesignSystemWriter;
|
||||||
|
use LIT\MCP\Integrations\Divi\Design\FontCatalog;
|
||||||
use LIT\MCP\Integrations\Divi\Design\GlobalColorsAdapter;
|
use LIT\MCP\Integrations\Divi\Design\GlobalColorsAdapter;
|
||||||
use LIT\MCP\Integrations\Divi\Design\GlobalVariablesAdapter;
|
use LIT\MCP\Integrations\Divi\Design\GlobalVariablesAdapter;
|
||||||
|
use LIT\MCP\Integrations\Divi\Design\ThemeOptionsAdapter;
|
||||||
use LIT\MCP\Integrations\Divi\Library\DiviLibraryStore;
|
use LIT\MCP\Integrations\Divi\Library\DiviLibraryStore;
|
||||||
use LIT\MCP\Integrations\Divi\Library\DiviRoles;
|
use LIT\MCP\Integrations\Divi\Library\DiviRoles;
|
||||||
use LIT\MCP\Integrations\Divi\Pages\DiviPages;
|
use LIT\MCP\Integrations\Divi\Pages\DiviPages;
|
||||||
@@ -31,6 +33,8 @@ final class DiviAdapters
|
|||||||
DesignSystemWriter::class,
|
DesignSystemWriter::class,
|
||||||
GlobalColorsAdapter::class,
|
GlobalColorsAdapter::class,
|
||||||
GlobalVariablesAdapter::class,
|
GlobalVariablesAdapter::class,
|
||||||
|
ThemeOptionsAdapter::class,
|
||||||
|
FontCatalog::class,
|
||||||
DiviModules::class,
|
DiviModules::class,
|
||||||
DynamicContentMap::class,
|
DynamicContentMap::class,
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -23,18 +23,21 @@ enum DiviFeature: string
|
|||||||
case PresetWrite = 'preset-write';
|
case PresetWrite = 'preset-write';
|
||||||
case ColorWrite = 'color-write';
|
case ColorWrite = 'color-write';
|
||||||
case VariableWrite = 'variable-write';
|
case VariableWrite = 'variable-write';
|
||||||
|
case ThemeOptionRead = 'theme-option-read';
|
||||||
|
case ThemeOptionWrite = 'theme-option-write';
|
||||||
|
case FontUpload = 'font-upload';
|
||||||
case LibraryRead = 'library-read';
|
case LibraryRead = 'library-read';
|
||||||
case LibraryWrite = 'library-write';
|
case LibraryWrite = 'library-write';
|
||||||
case Roles = 'roles';
|
case Roles = 'roles';
|
||||||
case ThemeBuilderRead = 'tb-read';
|
case ThemeBuilderRead = 'tb-read';
|
||||||
case ThemeBuilderWrite = 'tb-write';
|
case ThemeBuilderWrite = 'tb-write';
|
||||||
|
|
||||||
/** Whether the feature changes data (the six *Write cases). */
|
/** Whether the feature changes data (the seven *Write cases and FontUpload). */
|
||||||
public function isWrite(): bool
|
public function isWrite(): bool
|
||||||
{
|
{
|
||||||
return match ($this) {
|
return match ($this) {
|
||||||
self::PageWrite, self::PresetWrite, self::ColorWrite, self::VariableWrite, self::LibraryWrite,
|
self::PageWrite, self::PresetWrite, self::ColorWrite, self::VariableWrite, self::ThemeOptionWrite,
|
||||||
self::ThemeBuilderWrite => true,
|
self::FontUpload, self::LibraryWrite, self::ThemeBuilderWrite => true,
|
||||||
default => false,
|
default => false,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -51,6 +54,9 @@ enum DiviFeature: string
|
|||||||
self::PresetWrite => 'Presets schreiben',
|
self::PresetWrite => 'Presets schreiben',
|
||||||
self::ColorWrite => 'Farben schreiben',
|
self::ColorWrite => 'Farben schreiben',
|
||||||
self::VariableWrite => 'Variablen schreiben',
|
self::VariableWrite => 'Variablen schreiben',
|
||||||
|
self::ThemeOptionRead => 'Theme-Optionen lesen',
|
||||||
|
self::ThemeOptionWrite => 'Theme-Optionen schreiben',
|
||||||
|
self::FontUpload => 'Schriften hochladen',
|
||||||
self::LibraryRead => 'Bibliothek lesen',
|
self::LibraryRead => 'Bibliothek lesen',
|
||||||
self::LibraryWrite => 'Bibliothek schreiben',
|
self::LibraryWrite => 'Bibliothek schreiben',
|
||||||
self::Roles => 'Divi-Rollen',
|
self::Roles => 'Divi-Rollen',
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
{
|
{
|
||||||
|
"customizer-typography": "0264fa5a2ba604542e1e1ad0c0339f04c8f4f67297087a0f92954ee63d343fd0",
|
||||||
"library-submit-layout": "594b3843d4c42e1a91e5eda06f108c79583a0036e46c92dd749f4f541e362d02",
|
"library-submit-layout": "594b3843d4c42e1a91e5eda06f108c79583a0036e46c92dd749f4f541e362d02",
|
||||||
"preset-sync": "9586f8005b8065893c0350977a58ee9171bf715fff21ac0c84663cc512f45522",
|
"preset-sync": "9586f8005b8065893c0350977a58ee9171bf715fff21ac0c84663cc512f45522",
|
||||||
"sig:ET\\Builder\\Framework\\UserRole\\UserRole::can_current_user_use_visual_builder": "76d199c13adc6600c8a456225a8eb9db8037064f20eca9db597c4e45ccecf8d5",
|
"sig:ET\\Builder\\Framework\\UserRole\\UserRole::can_current_user_use_visual_builder": "76d199c13adc6600c8a456225a8eb9db8037064f20eca9db597c4e45ccecf8d5",
|
||||||
@@ -24,8 +25,19 @@
|
|||||||
"sig:ET_Core_PageResource::remove_static_resources": "eaa39fd2c037c2d399578f71862da5c04b68d58a30657916482f95e94e5d8a96",
|
"sig:ET_Core_PageResource::remove_static_resources": "eaa39fd2c037c2d399578f71862da5c04b68d58a30657916482f95e94e5d8a96",
|
||||||
"sig:et_": "ae7b0ec701e388b03d29d972195cb727fe1c8fd75dba4657ad71f1a963a32966",
|
"sig:et_": "ae7b0ec701e388b03d29d972195cb727fe1c8fd75dba4657ad71f1a963a32966",
|
||||||
"sig:et_builder_enabled_for_post_type": "a6aed8043b2c07ef1082488fde706816cf0146847f5bac8acca79fb3fe5275fc",
|
"sig:et_builder_enabled_for_post_type": "a6aed8043b2c07ef1082488fde706816cf0146847f5bac8acca79fb3fe5275fc",
|
||||||
|
"sig:et_builder_get_custom_fonts": "ae7b0ec701e388b03d29d972195cb727fe1c8fd75dba4657ad71f1a963a32966",
|
||||||
|
"sig:et_builder_get_fonts": "3ac3132305f3818c13df78416ae13e34c36af17a0ba9b66d652fe9babc6c051d",
|
||||||
|
"sig:et_builder_get_google_fonts": "ae7b0ec701e388b03d29d972195cb727fe1c8fd75dba4657ad71f1a963a32966",
|
||||||
"sig:et_builder_get_public_post_types": "ae7b0ec701e388b03d29d972195cb727fe1c8fd75dba4657ad71f1a963a32966",
|
"sig:et_builder_get_public_post_types": "ae7b0ec701e388b03d29d972195cb727fe1c8fd75dba4657ad71f1a963a32966",
|
||||||
|
"sig:et_builder_get_websafe_fonts": "ae7b0ec701e388b03d29d972195cb727fe1c8fd75dba4657ad71f1a963a32966",
|
||||||
|
"sig:et_delete_option": "a6aed8043b2c07ef1082488fde706816cf0146847f5bac8acca79fb3fe5275fc",
|
||||||
|
"sig:et_divi_global_font_style": "a6aed8043b2c07ef1082488fde706816cf0146847f5bac8acca79fb3fe5275fc",
|
||||||
|
"sig:et_pb_force_regenerate_templates": "ae7b0ec701e388b03d29d972195cb727fe1c8fd75dba4657ad71f1a963a32966",
|
||||||
"sig:et_pb_is_allowed": "b7561d91c3e696079a598474517118c3e9809cbf1f08b1321babe7af11d15799",
|
"sig:et_pb_is_allowed": "b7561d91c3e696079a598474517118c3e9809cbf1f08b1321babe7af11d15799",
|
||||||
|
"sig:et_sanitize_float_number": "a6aed8043b2c07ef1082488fde706816cf0146847f5bac8acca79fb3fe5275fc",
|
||||||
|
"sig:et_sanitize_font_choices": "a6aed8043b2c07ef1082488fde706816cf0146847f5bac8acca79fb3fe5275fc",
|
||||||
|
"sig:et_sanitize_global_font_weight": "a6aed8043b2c07ef1082488fde706816cf0146847f5bac8acca79fb3fe5275fc",
|
||||||
|
"sig:et_sanitize_int_number": "a6aed8043b2c07ef1082488fde706816cf0146847f5bac8acca79fb3fe5275fc",
|
||||||
"sig:et_theme_builder_clear_wp_cache": "3ac3132305f3818c13df78416ae13e34c36af17a0ba9b66d652fe9babc6c051d",
|
"sig:et_theme_builder_clear_wp_cache": "3ac3132305f3818c13df78416ae13e34c36af17a0ba9b66d652fe9babc6c051d",
|
||||||
"sig:et_theme_builder_get_flat_template_settings_options": "ae7b0ec701e388b03d29d972195cb727fe1c8fd75dba4657ad71f1a963a32966",
|
"sig:et_theme_builder_get_flat_template_settings_options": "ae7b0ec701e388b03d29d972195cb727fe1c8fd75dba4657ad71f1a963a32966",
|
||||||
"sig:et_theme_builder_get_template": "a6aed8043b2c07ef1082488fde706816cf0146847f5bac8acca79fb3fe5275fc",
|
"sig:et_theme_builder_get_template": "a6aed8043b2c07ef1082488fde706816cf0146847f5bac8acca79fb3fe5275fc",
|
||||||
@@ -33,6 +45,7 @@
|
|||||||
"sig:et_theme_builder_get_theme_builder_template_ids": "b7561d91c3e696079a598474517118c3e9809cbf1f08b1321babe7af11d15799",
|
"sig:et_theme_builder_get_theme_builder_template_ids": "b7561d91c3e696079a598474517118c3e9809cbf1f08b1321babe7af11d15799",
|
||||||
"sig:et_theme_builder_insert_layout": "a6aed8043b2c07ef1082488fde706816cf0146847f5bac8acca79fb3fe5275fc",
|
"sig:et_theme_builder_insert_layout": "a6aed8043b2c07ef1082488fde706816cf0146847f5bac8acca79fb3fe5275fc",
|
||||||
"sig:et_theme_builder_store_template": "6c84ec472739a2dbb87bcf21777c4dc977fd2233142eacfefe60eba462a4c219",
|
"sig:et_theme_builder_store_template": "6c84ec472739a2dbb87bcf21777c4dc977fd2233142eacfefe60eba462a4c219",
|
||||||
|
"sig:et_update_option": "b6b87f370cc0a0e01df5899074045bc8646b05855d4cd7103d9fc1ca0ac09799",
|
||||||
"tb-api-save": "7ba452f4d73c9b676281ca6ddcfddd669502965db868bf478ef7804937e5bfc4",
|
"tb-api-save": "7ba452f4d73c9b676281ca6ddcfddd669502965db868bf478ef7804937e5bfc4",
|
||||||
"tb-load-setting-options": "2ea0a6925a1da5522489a417104b384b2bd77a16b25f4122b9503873a4c8a24e",
|
"tb-load-setting-options": "2ea0a6925a1da5522489a417104b384b2bd77a16b25f4122b9503873a4c8a24e",
|
||||||
"tb-store-template": "3bf4ac6028ed99ed8ac3523c659392a5fce7818ef8a80ea226e01f5404eb441d",
|
"tb-store-template": "3bf4ac6028ed99ed8ac3523c659392a5fce7818ef8a80ea226e01f5404eb441d",
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -27,8 +27,8 @@ trap 'rc=$?; cleanup; guard_exit "$rc" || exit $?' EXIT
|
|||||||
|
|
||||||
check_pages() { wp post list --post_type=page --post_status=any --meta_key=_lit_mcp_check_page --format=count; }
|
check_pages() { wp post list --post_type=page --post_status=any --meta_key=_lit_mcp_check_page --format=count; }
|
||||||
|
|
||||||
# The ids of the thirteen features (plan decision D1, plan 4c V2), sorted.
|
# The ids of the sixteen features (plan decision D1, plan 4c V2, plan 4d W1), sorted.
|
||||||
FEATURE_IDS='["catalog","color-write","design-read","dynamic-sources","library-read","library-write","page-read","page-write","preset-write","roles","tb-read","tb-write","variable-write"]'
|
FEATURE_IDS='["catalog","color-write","design-read","dynamic-sources","font-upload","library-read","library-write","page-read","page-write","preset-write","roles","tb-read","tb-write","theme-option-read","theme-option-write","variable-write"]'
|
||||||
|
|
||||||
# Adds a problem to one feature in the cached state, as if its mirror check had failed. Needs the cache entry of
|
# Adds a problem to one feature in the cached state, as if its mirror check had failed. Needs the cache entry of
|
||||||
# ReleaseResolver (format 2: ["format" => 2, "key" => stateKey, "problems" => list of Problem::toArray()]); the key
|
# ReleaseResolver (format 2: ["format" => 2, "key" => stateKey, "problems" => list of Problem::toArray()]); the key
|
||||||
@@ -68,7 +68,7 @@ echo "$STATUS" | jq -e --argjson ids "$FEATURE_IDS" '
|
|||||||
.components[] | select(.id == "divi") | .features
|
.components[] | select(.id == "divi") | .features
|
||||||
| ([.[].id] | sort) == $ids
|
| ([.[].id] | sort) == $ids
|
||||||
and all(.state == "released" and .source == "code" and .problems == [])' >/dev/null \
|
and all(.state == "released" and .source == "code" and .problems == [])' >/dev/null \
|
||||||
|| fail "all thirteen features must be released from code without problems: $STATUS"
|
|| fail "all sixteen features must be released from code without problems: $STATUS"
|
||||||
echo "$STATUS" | jq -e '
|
echo "$STATUS" | jq -e '
|
||||||
.components[] | select(.id == "divi") | .features
|
.components[] | select(.id == "divi") | .features
|
||||||
| (.[] | select(.id == "page-write") | .tools | any(. == "lit-divi-insert"))
|
| (.[] | select(.id == "page-write") | .tools | any(. == "lit-divi-insert"))
|
||||||
@@ -76,7 +76,7 @@ echo "$STATUS" | jq -e '
|
|||||||
and (.[] | select(.id == "preset-write") | .tools | any(startswith("lit/divi-presets-")))
|
and (.[] | select(.id == "preset-write") | .tools | any(startswith("lit/divi-presets-")))
|
||||||
and (.[] | select(.id == "page-read") | .label == "Seiten lesen")' >/dev/null \
|
and (.[] | select(.id == "page-read") | .label == "Seiten lesen")' >/dev/null \
|
||||||
|| fail "features must name their tools (direct lit-<name>, indirect lit/<name>): $STATUS"
|
|| fail "features must name their tools (direct lit-<name>, indirect lit/<name>): $STATUS"
|
||||||
pass "lit-site-status: thirteen features released (code), each with its tools"
|
pass "lit-site-status: sixteen features released (code), each with its tools"
|
||||||
|
|
||||||
# 2. Admin check through the REST route (application password instead of the cookie nonce).
|
# 2. Admin check through the REST route (application password instead of the cookie nonce).
|
||||||
# X-Forwarded-Proto: https is required here: without it the request to http://localhost with the
|
# X-Forwarded-Proto: https is required here: without it the request to http://localhost with the
|
||||||
@@ -212,8 +212,8 @@ simulate_problem page-write "Smoke: Cache vergiftet"
|
|||||||
CLI=$(wp --user="$ADMIN_LOGIN" lit-mcp divi-check 2>&1) || fail "wp lit-mcp divi-check must succeed (exit 0): $CLI"
|
CLI=$(wp --user="$ADMIN_LOGIN" lit-mcp divi-check 2>&1) || fail "wp lit-mcp divi-check must succeed (exit 0): $CLI"
|
||||||
echo "$CLI" | grep -q '^Fähigkeiten:' || fail "divi-check must print the matrix: $CLI"
|
echo "$CLI" | grep -q '^Fähigkeiten:' || fail "divi-check must print the matrix: $CLI"
|
||||||
echo "$CLI" | grep -q '✓ Seiten schreiben – freigegeben (code)' || fail "divi-check must show page-write released: $CLI"
|
echo "$CLI" | grep -q '✓ Seiten schreiben – freigegeben (code)' || fail "divi-check must show page-write released: $CLI"
|
||||||
[ "$(echo "$CLI" | grep -c ' – freigegeben (')" = "13" ] || fail "divi-check must show thirteen released features: $CLI"
|
[ "$(echo "$CLI" | grep -c ' – freigegeben (')" = "16" ] || fail "divi-check must show sixteen released features: $CLI"
|
||||||
[ "$(check_pages)" = "0" ] || fail "the CLI check must delete its page"
|
[ "$(check_pages)" = "0" ] || fail "the CLI check must delete its page"
|
||||||
pass "wp lit-mcp divi-check prints thirteen released features, recomputes the state and leaves no page"
|
pass "wp lit-mcp divi-check prints sixteen released features, recomputes the state and leaves no page"
|
||||||
|
|
||||||
finish
|
finish
|
||||||
|
|||||||
@@ -228,14 +228,14 @@ final class DiviCheckCommandTest extends TestCase
|
|||||||
self::assertSame(0, $code);
|
self::assertSame(0, $code);
|
||||||
self::assertSame(
|
self::assertSame(
|
||||||
[
|
[
|
||||||
'library-submit-layout', 'preset-sync', 'sig:A::b', 'sig:et_pb_is_allowed', 'tb-api-save',
|
'customizer-typography', 'library-submit-layout', 'preset-sync', 'sig:A::b', 'sig:et_pb_is_allowed',
|
||||||
'tb-load-setting-options', 'tb-store-template', 'vb-sync-flags',
|
'tb-api-save', 'tb-load-setting-options', 'tb-store-template', 'vb-sync-flags',
|
||||||
],
|
],
|
||||||
array_keys(DiviReference::expected($this->expectedFile))
|
array_keys(DiviReference::expected($this->expectedFile))
|
||||||
);
|
);
|
||||||
self::assertSame(str_repeat('b', 64), DiviReference::expected($this->expectedFile)['sig:A::b']);
|
self::assertSame(str_repeat('b', 64), DiviReference::expected($this->expectedFile)['sig:A::b']);
|
||||||
self::assertContains(
|
self::assertContains(
|
||||||
'Referenz aktualisiert (6 Fingerabdrücke, 2 Signaturen). Smoke-Skripte laufen lassen, dann committen.',
|
'Referenz aktualisiert (7 Fingerabdrücke, 2 Signaturen). Smoke-Skripte laufen lassen, dann committen.',
|
||||||
$this->lines
|
$this->lines
|
||||||
);
|
);
|
||||||
self::assertStringStartsWith(
|
self::assertStringStartsWith(
|
||||||
@@ -244,10 +244,11 @@ final class DiviCheckCommandTest extends TestCase
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Minimal stand-ins for the six mirrored Divi functions at their real paths. */
|
/** Minimal stand-ins for the seven mirrored Divi functions at their real paths. */
|
||||||
private function writeDivi(): void
|
private function writeDivi(): void
|
||||||
{
|
{
|
||||||
$files = [
|
$files = [
|
||||||
|
'functions.php' => "function et_divi_customizer_theme_settings( \$wp_customize ) {\n}",
|
||||||
'includes/builder/shortcode-core.php' => "function et_pb_submit_layout( \$args ) {\n\treturn 1;\n}",
|
'includes/builder/shortcode-core.php' => "function et_pb_submit_layout( \$args ) {\n\treturn 1;\n}",
|
||||||
'includes/builder/frontend-builder/theme-builder/api.php' => "function et_theme_builder_api_save() {\n}",
|
'includes/builder/frontend-builder/theme-builder/api.php' => "function et_theme_builder_api_save() {\n}",
|
||||||
'includes/builder/frontend-builder/theme-builder/theme-builder.php' =>
|
'includes/builder/frontend-builder/theme-builder/theme-builder.php' =>
|
||||||
|
|||||||
@@ -7,7 +7,9 @@ namespace LIT\MCP\Tests\Unit\Integrations\Divi\Design;
|
|||||||
use Brain\Monkey\Functions;
|
use Brain\Monkey\Functions;
|
||||||
use LIT\MCP\Integrations\Divi\Design\DesignRights;
|
use LIT\MCP\Integrations\Divi\Design\DesignRights;
|
||||||
use LIT\MCP\Integrations\Divi\Design\DesignSystemWriter;
|
use LIT\MCP\Integrations\Divi\Design\DesignSystemWriter;
|
||||||
|
use LIT\MCP\Integrations\Divi\Design\FontCatalog;
|
||||||
use LIT\MCP\Integrations\Divi\Design\GlobalVariablesAdapter;
|
use LIT\MCP\Integrations\Divi\Design\GlobalVariablesAdapter;
|
||||||
|
use LIT\MCP\Integrations\Divi\Design\ThemeOptionsAdapter;
|
||||||
use LIT\MCP\Tests\Unit\TestCase;
|
use LIT\MCP\Tests\Unit\TestCase;
|
||||||
use Mockery;
|
use Mockery;
|
||||||
|
|
||||||
@@ -73,4 +75,46 @@ final class DesignRightsTest extends TestCase
|
|||||||
$refusal = self::assertToolError(DesignRights::variables($writer, $variables), 'forbidden');
|
$refusal = self::assertToolError(DesignRights::variables($writer, $variables), 'forbidden');
|
||||||
self::assertStringContainsString('Variablen ändern erfordert', $refusal['message']);
|
self::assertStringContainsString('Variablen ändern erfordert', $refusal['message']);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function testThemeOptionsNeedEditThemeOptionsAndTheThemeCustomizerRight(): void
|
||||||
|
{
|
||||||
|
$writer = Mockery::mock(DesignSystemWriter::class);
|
||||||
|
// Divi's customizer does not need the Visual Builder (plan 4d, rights).
|
||||||
|
$writer->expects('canUseVisualBuilder')->never();
|
||||||
|
$options = Mockery::mock(ThemeOptionsAdapter::class);
|
||||||
|
$options->allows('canCustomize')->andReturn(true, false);
|
||||||
|
Functions\when('current_user_can')->alias(static fn (string $cap): bool => $cap === 'edit_theme_options');
|
||||||
|
|
||||||
|
self::assertNull(DesignRights::themeOptions($writer, $options));
|
||||||
|
$refusal = self::assertToolError(DesignRights::themeOptions($writer, $options), 'forbidden');
|
||||||
|
self::assertStringContainsString('Theme-Anpassung', $refusal['message']);
|
||||||
|
self::assertStringContainsString('keine Theme-Optionen', $refusal['message']);
|
||||||
|
self::assertStringContainsString('Rollen-Editor', $refusal['hint']);
|
||||||
|
|
||||||
|
Functions\when('current_user_can')->justReturn(false);
|
||||||
|
$refusal = self::assertToolError(DesignRights::themeOptions($writer, $options), 'forbidden');
|
||||||
|
self::assertStringContainsString('Theme-Optionen ändern erfordert', $refusal['message']);
|
||||||
|
self::assertStringContainsString('edit_theme_options', $refusal['message']);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testFontsNeedUploadFilesEditThemeOptionsAndTheFontsRight(): void
|
||||||
|
{
|
||||||
|
$fonts = Mockery::mock(FontCatalog::class);
|
||||||
|
$fonts->allows('canManage')->andReturn(true, false);
|
||||||
|
Functions\when('current_user_can')->justReturn(true);
|
||||||
|
|
||||||
|
self::assertNull(DesignRights::fonts($fonts));
|
||||||
|
$refusal = self::assertToolError(DesignRights::fonts($fonts), 'forbidden');
|
||||||
|
self::assertStringContainsString('Eigene Schriften verwalten', $refusal['message']);
|
||||||
|
self::assertStringContainsString('Rollen-Editor', $refusal['hint']);
|
||||||
|
|
||||||
|
Functions\when('current_user_can')->alias(static fn (string $cap): bool => $cap === 'edit_theme_options');
|
||||||
|
$refusal = self::assertToolError(DesignRights::fonts($fonts), 'forbidden');
|
||||||
|
self::assertStringContainsString('upload_files', $refusal['message']);
|
||||||
|
|
||||||
|
Functions\when('current_user_can')->alias(static fn (string $cap): bool => $cap === 'upload_files');
|
||||||
|
$refusal = self::assertToolError(DesignRights::fonts($fonts), 'forbidden');
|
||||||
|
self::assertStringContainsString('Schriften hochladen erfordert', $refusal['message']);
|
||||||
|
self::assertStringContainsString('edit_theme_options', $refusal['message']);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,98 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace LIT\MCP\Tests\Unit\Integrations\Divi\Design;
|
||||||
|
|
||||||
|
use Brain\Monkey\Functions;
|
||||||
|
use LIT\MCP\Integrations\Divi\Design\FontCatalog;
|
||||||
|
use LIT\MCP\Integrations\Divi\Release\Call;
|
||||||
|
use LIT\MCP\Integrations\Divi\Release\DiviAdapters;
|
||||||
|
use LIT\MCP\Integrations\Divi\Release\DiviFeature;
|
||||||
|
use LIT\MCP\Integrations\Divi\Release\Manifest;
|
||||||
|
use LIT\MCP\Tests\Unit\TestCase;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Divi's font lists and et_pb_is_allowed() are faked with Brain Monkey; those tests run in their own process (Brain
|
||||||
|
* Monkey defines a missing function for the rest of the process, and other tests rely on Divi's functions being
|
||||||
|
* absent).
|
||||||
|
*/
|
||||||
|
final class FontCatalogTest extends TestCase
|
||||||
|
{
|
||||||
|
public function testIsARegisteredDesignAdapterForTheFontLists(): void
|
||||||
|
{
|
||||||
|
$manifest = FontCatalog::manifest();
|
||||||
|
|
||||||
|
self::assertContains(FontCatalog::class, DiviAdapters::CLASSES);
|
||||||
|
self::assertSame(Manifest::DESIGN, $manifest->area);
|
||||||
|
self::assertSame(
|
||||||
|
[
|
||||||
|
'et_builder_get_fonts()', 'et_builder_get_custom_fonts()', 'et_builder_get_google_fonts()',
|
||||||
|
'et_builder_get_websafe_fonts()', 'et_pb_is_allowed()',
|
||||||
|
],
|
||||||
|
array_map(static fn (Call $call): string => $call->label(), $manifest->calls)
|
||||||
|
);
|
||||||
|
self::assertSame([0, 0, 0, 0, 1], array_map(static fn (Call $call): ?int => $call->args, $manifest->calls));
|
||||||
|
self::assertSame(['et_uploaded_fonts', 'et_google_fonts_cache', 'et_google_api_settings'], $manifest->keys);
|
||||||
|
// Plan 4d W1: a broken font list locks every tool that checks font names against it.
|
||||||
|
self::assertSame(
|
||||||
|
[DiviFeature::ThemeOptionWrite, DiviFeature::FontUpload, DiviFeature::VariableWrite],
|
||||||
|
$manifest->features
|
||||||
|
);
|
||||||
|
self::assertSame([DiviFeature::FontUpload], $manifest->featuresOf($manifest->calls[4]));
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @runInSeparateProcess */
|
||||||
|
public function testNamesAreTheExactFamiliesOfDivisSanitizerLoadedOnce(): void
|
||||||
|
{
|
||||||
|
Functions\expect('et_builder_get_custom_fonts')->once()->andReturn(['Hausschrift' => [], '1942' => []]);
|
||||||
|
Functions\expect('et_builder_get_fonts')->once()
|
||||||
|
->andReturn(['Arial' => [], 'Open Sans' => [], 'Roboto' => []]);
|
||||||
|
$catalog = new FontCatalog();
|
||||||
|
|
||||||
|
self::assertSame(['Hausschrift', '1942', 'Arial', 'Open Sans', 'Roboto'], $catalog->names());
|
||||||
|
self::assertTrue($catalog->has('Roboto'));
|
||||||
|
self::assertTrue($catalog->has('1942'), 'a numeric family name stays a string');
|
||||||
|
self::assertFalse($catalog->has('roboto'), 'exact, like Divi');
|
||||||
|
self::assertFalse($catalog->has('none'), 'Divi stores the default font as false, never as a name');
|
||||||
|
self::assertSame($catalog->names(), $catalog->names());
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @runInSeparateProcess */
|
||||||
|
public function testUploadedAndReservedAreReadFreshEachTime(): void
|
||||||
|
{
|
||||||
|
Functions\expect('et_builder_get_custom_fonts')->twice()
|
||||||
|
->andReturn(['Hausschrift' => []], ['Hausschrift' => [], 'Neu' => []]);
|
||||||
|
Functions\when('et_builder_get_google_fonts')->justReturn(['Roboto' => []]);
|
||||||
|
Functions\when('et_builder_get_websafe_fonts')->justReturn(['Arial' => [], 'Roboto' => []]);
|
||||||
|
$catalog = new FontCatalog();
|
||||||
|
|
||||||
|
self::assertSame(['Hausschrift'], $catalog->uploaded());
|
||||||
|
self::assertSame(['Roboto', 'Arial', 'Hausschrift', 'Neu'], $catalog->reserved(), 'after an upload');
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @runInSeparateProcess */
|
||||||
|
public function testABrokenListCountsAsEmpty(): void
|
||||||
|
{
|
||||||
|
// A filter of et_builder_google_fonts that returns no array (Divi passes it on unchecked).
|
||||||
|
Functions\when('et_builder_get_google_fonts')->justReturn(null);
|
||||||
|
Functions\when('et_builder_get_websafe_fonts')->justReturn(['Arial' => []]);
|
||||||
|
Functions\when('et_builder_get_custom_fonts')->justReturn([]);
|
||||||
|
|
||||||
|
self::assertSame(['Arial'], (new FontCatalog())->reserved());
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testWithoutDiviNobodyManagesFonts(): void
|
||||||
|
{
|
||||||
|
// Unit tests have no et_pb_is_allowed(): a missing Divi function never means "allowed".
|
||||||
|
self::assertFalse((new FontCatalog())->canManage());
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @runInSeparateProcess */
|
||||||
|
public function testCanManageAsksTheCustomFontsRight(): void
|
||||||
|
{
|
||||||
|
Functions\when('et_pb_is_allowed')->alias(static fn (string $cap): bool => $cap === 'custom_fonts_management');
|
||||||
|
|
||||||
|
self::assertTrue((new FontCatalog())->canManage());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,246 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace LIT\MCP\Tests\Unit\Integrations\Divi\Design;
|
||||||
|
|
||||||
|
use Brain\Monkey\Functions;
|
||||||
|
use InvalidArgumentException;
|
||||||
|
use LIT\MCP\Integrations\Divi\Design\ThemeOptionsAdapter;
|
||||||
|
use LIT\MCP\Integrations\Divi\Release\Call;
|
||||||
|
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 RuntimeException;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Divi's sanitizers, et_update_option(), et_delete_option(), et_pb_force_regenerate_templates() and
|
||||||
|
* et_pb_is_allowed() are faked with Brain Monkey; those tests run in their own process (Brain Monkey defines a missing
|
||||||
|
* function for the rest of the process, and other tests rely on Divi's functions being absent). ET_Core_PageResource
|
||||||
|
* is the recording stub of tests/Stubs.
|
||||||
|
*/
|
||||||
|
final class ThemeOptionsAdapterTest extends TestCase
|
||||||
|
{
|
||||||
|
/** Plan 4d W2, in this order. */
|
||||||
|
private const KEYS = [
|
||||||
|
'heading_font', 'body_font', 'primary_nav_font', 'secondary_nav_font', 'heading_font_weight',
|
||||||
|
'body_font_weight', 'body_font_size', 'tablet_body_font_size', 'phone_body_font_size', 'body_header_size',
|
||||||
|
'tablet_header_font_size', 'phone_header_font_size', 'body_font_height', 'body_header_height',
|
||||||
|
'body_header_spacing', 'body_header_style', 'content_width', 'gutter_width', 'section_padding', 'row_padding',
|
||||||
|
];
|
||||||
|
|
||||||
|
public function testIsARegisteredDesignAdapterForThemeOptions(): void
|
||||||
|
{
|
||||||
|
$manifest = ThemeOptionsAdapter::manifest();
|
||||||
|
|
||||||
|
self::assertContains(ThemeOptionsAdapter::class, DiviAdapters::CLASSES);
|
||||||
|
self::assertSame(Manifest::DESIGN, $manifest->area);
|
||||||
|
self::assertSame(
|
||||||
|
[
|
||||||
|
'et_sanitize_font_choices()', 'et_sanitize_global_font_weight()', 'et_sanitize_float_number()',
|
||||||
|
'et_sanitize_int_number()', 'et_divi_global_font_style()', 'et_update_option()', 'et_delete_option()',
|
||||||
|
'ET_Core_PageResource::remove_static_resources()', 'et_pb_force_regenerate_templates()',
|
||||||
|
'et_pb_is_allowed()',
|
||||||
|
],
|
||||||
|
array_map(static fn (Call $call): string => $call->label(), $manifest->calls)
|
||||||
|
);
|
||||||
|
self::assertSame(
|
||||||
|
[1, 1, 1, 1, 1, 2, 1, 2, 0, 1],
|
||||||
|
array_map(static fn (Call $call): ?int => $call->args, $manifest->calls)
|
||||||
|
);
|
||||||
|
self::assertSame(['et_divi'], $manifest->keys);
|
||||||
|
self::assertSame([DiviFeature::ThemeOptionWrite], $manifest->features);
|
||||||
|
|
||||||
|
self::assertCount(1, $manifest->mirrors);
|
||||||
|
$mirror = $manifest->mirrors[0];
|
||||||
|
self::assertSame('customizer-typography', $mirror->id);
|
||||||
|
self::assertSame('functions.php', $mirror->file);
|
||||||
|
self::assertSame('et_divi_customizer_theme_settings', $mirror->function);
|
||||||
|
self::assertNull($mirror->class);
|
||||||
|
self::assertSame(
|
||||||
|
[DiviFeature::ThemeOptionRead, DiviFeature::ThemeOptionWrite],
|
||||||
|
$manifest->featuresOf($mirror)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testTheSanitizerTableCoversTheWhitelistWithDivisCallsOfTheManifest(): void
|
||||||
|
{
|
||||||
|
self::assertSame(self::KEYS, array_keys(ThemeOptionsAdapter::SANITIZERS));
|
||||||
|
|
||||||
|
$divi = array_values(array_unique(array_diff(ThemeOptionsAdapter::SANITIZERS, ['absint'])));
|
||||||
|
sort($divi);
|
||||||
|
self::assertSame(
|
||||||
|
[
|
||||||
|
'et_divi_global_font_style', 'et_sanitize_float_number', 'et_sanitize_font_choices',
|
||||||
|
'et_sanitize_global_font_weight', 'et_sanitize_int_number',
|
||||||
|
],
|
||||||
|
$divi
|
||||||
|
);
|
||||||
|
$calls = array_map(
|
||||||
|
static fn (Call $call): string => $call->symbol . '/' . $call->args,
|
||||||
|
ThemeOptionsAdapter::manifest()->calls
|
||||||
|
);
|
||||||
|
foreach ($divi as $sanitizer) {
|
||||||
|
self::assertContains($sanitizer . '/1', $calls, 'every Divi sanitizer is a call of the manifest');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testSanitizeRefusesAKeyOutsideTheWhitelist(): void
|
||||||
|
{
|
||||||
|
$this->expectException(InvalidArgumentException::class);
|
||||||
|
$this->expectExceptionMessage('not a theme option of the whitelist: accent_color');
|
||||||
|
|
||||||
|
(new ThemeOptionsAdapter())->sanitize('accent_color', '#ff0000');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testSizesGoThroughWordPressAbsintAndNonScalarsAreRefused(): void
|
||||||
|
{
|
||||||
|
$adapter = new ThemeOptionsAdapter();
|
||||||
|
|
||||||
|
self::assertSame(15, $adapter->sanitize('body_font_size', '15.7'), 'absint rounds, the caller compares');
|
||||||
|
self::assertSame(1200, $adapter->sanitize('content_width', 1200));
|
||||||
|
self::assertFalse($adapter->sanitize('row_padding', ['2']));
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @runInSeparateProcess */
|
||||||
|
public function testSanitizeCallsDivisCustomizerSanitizerOfTheKey(): void
|
||||||
|
{
|
||||||
|
Functions\expect('et_sanitize_font_choices')->once()->with('Roboto')->andReturn('Roboto');
|
||||||
|
Functions\expect('et_sanitize_global_font_weight')->once()->with('600')->andReturn('600');
|
||||||
|
Functions\expect('et_sanitize_float_number')->once()->with('1.8')->andReturn(1.8);
|
||||||
|
Functions\expect('et_sanitize_int_number')->once()->with('-1')->andReturn(-1);
|
||||||
|
Functions\expect('et_divi_global_font_style')->once()->with('italic|uppercase')->andReturn(false);
|
||||||
|
$adapter = new ThemeOptionsAdapter();
|
||||||
|
|
||||||
|
self::assertSame('Roboto', $adapter->sanitize('secondary_nav_font', 'Roboto'));
|
||||||
|
self::assertSame('600', $adapter->sanitize('body_font_weight', '600'));
|
||||||
|
self::assertSame(1.8, $adapter->sanitize('body_header_height', '1.8'));
|
||||||
|
self::assertSame(-1, $adapter->sanitize('body_header_spacing', '-1'));
|
||||||
|
self::assertFalse($adapter->sanitize('body_header_style', 'italic|uppercase'), 'Divi refuses combinations');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testReadReturnsThePresentWhitelistKeysWithTheirStoredTypes(): void
|
||||||
|
{
|
||||||
|
Functions\when('get_option')->alias(static fn (string $name): mixed => $name === 'et_divi'
|
||||||
|
? [
|
||||||
|
'row_padding' => null,
|
||||||
|
'accent_color' => '#ff0000',
|
||||||
|
'body_font' => 'Roboto',
|
||||||
|
'body_font_height' => 1.8,
|
||||||
|
'gutter_width' => ['3'],
|
||||||
|
'heading_font' => false,
|
||||||
|
'body_font_size' => 15,
|
||||||
|
]
|
||||||
|
: false);
|
||||||
|
|
||||||
|
self::assertSame(
|
||||||
|
['heading_font' => false, 'body_font' => 'Roboto', 'body_font_size' => 15, 'body_font_height' => 1.8],
|
||||||
|
(new ThemeOptionsAdapter())->read(),
|
||||||
|
'whitelist only, in W2 order; values of another type are not theme option values'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testReadWithoutThemeOptionsIsEmpty(): void
|
||||||
|
{
|
||||||
|
Functions\when('get_option')->justReturn(false);
|
||||||
|
|
||||||
|
self::assertSame([], (new ThemeOptionsAdapter())->read());
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testWritesNeedAReleasedDivi(): void
|
||||||
|
{
|
||||||
|
ReleaseGate::useResolver(
|
||||||
|
static fn (): ReleaseStatus => ReleaseMatrices::uniform(ReleaseState::Untested, '5.16.0')
|
||||||
|
);
|
||||||
|
|
||||||
|
$this->expectException(DiviLocked::class);
|
||||||
|
|
||||||
|
(new ThemeOptionsAdapter())->write(['body_font_size' => 16], []);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testWriteRefusesAKeyOutsideTheWhitelistBeforeWriting(): void
|
||||||
|
{
|
||||||
|
// Unit tests have no et_update_option(): reaching it would be an Error, not this exception.
|
||||||
|
$this->expectException(InvalidArgumentException::class);
|
||||||
|
$this->expectExceptionMessage('not a theme option of the whitelist: accent_color, divi_logo');
|
||||||
|
|
||||||
|
(new ThemeOptionsAdapter())->write(['body_font_size' => 16, 'accent_color' => '#f00'], ['divi_logo']);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @runInSeparateProcess */
|
||||||
|
public function testWriteUpdatesAndDeletesPerKeyClearsTheCachesAndReadsBack(): void
|
||||||
|
{
|
||||||
|
$stored = ['body_font_size' => 14, 'heading_font' => 'Roboto', 'accent_color' => '#ff0000'];
|
||||||
|
$log = [];
|
||||||
|
Functions\when('get_option')->alias(static function (string $name) use (&$stored): mixed {
|
||||||
|
return $name === 'et_divi' ? $stored : false;
|
||||||
|
});
|
||||||
|
Functions\when('et_update_option')->alias(static function (string $key, mixed $value) use (
|
||||||
|
&$stored,
|
||||||
|
&$log
|
||||||
|
): void {
|
||||||
|
$stored[$key] = $value;
|
||||||
|
$log[] = 'update ' . $key;
|
||||||
|
});
|
||||||
|
Functions\when('et_delete_option')->alias(static function (string $key) use (&$stored, &$log): void {
|
||||||
|
unset($stored[$key]);
|
||||||
|
$log[] = 'delete ' . $key;
|
||||||
|
});
|
||||||
|
Functions\when('et_pb_force_regenerate_templates')->alias(static function () use (&$log): void {
|
||||||
|
$log[] = 'regenerate templates after ' . count(\ET_Core_PageResource::$removed) . ' static CSS clear';
|
||||||
|
});
|
||||||
|
\ET_Core_PageResource::$removed = [];
|
||||||
|
|
||||||
|
$result = (new ThemeOptionsAdapter())->write(
|
||||||
|
['body_font_size' => 16, 'body_font_height' => 1.8],
|
||||||
|
['heading_font']
|
||||||
|
);
|
||||||
|
|
||||||
|
self::assertSame(['body_font_size' => 16, 'body_font_height' => 1.8], $result);
|
||||||
|
self::assertSame(
|
||||||
|
[
|
||||||
|
'update body_font_size', 'update body_font_height', 'delete heading_font',
|
||||||
|
'regenerate templates after 1 static CSS clear',
|
||||||
|
],
|
||||||
|
$log
|
||||||
|
);
|
||||||
|
self::assertSame([['all', 'all', false]], \ET_Core_PageResource::$removed, 'like a customizer save');
|
||||||
|
self::assertSame('#ff0000', $stored['accent_color'], 'other theme options stay');
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @runInSeparateProcess */
|
||||||
|
public function testWriteThrowsWhenTheReadBackDiffers(): void
|
||||||
|
{
|
||||||
|
$stored = ['body_font_size' => 14, 'heading_font' => 'Roboto'];
|
||||||
|
Functions\when('get_option')->alias(static fn (string $name): mixed => $name === 'et_divi' ? $stored : false);
|
||||||
|
// Divi silently saves nothing (e.g. a filter or another process).
|
||||||
|
Functions\when('et_update_option')->justReturn(null);
|
||||||
|
Functions\when('et_delete_option')->justReturn(null);
|
||||||
|
Functions\when('et_pb_force_regenerate_templates')->justReturn(null);
|
||||||
|
|
||||||
|
$this->expectException(RuntimeException::class);
|
||||||
|
$this->expectExceptionMessage('theme options not written: body_font_size, heading_font');
|
||||||
|
|
||||||
|
(new ThemeOptionsAdapter())->write(['body_font_size' => 16], ['heading_font']);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testWithoutDiviNobodyCustomizesTheTheme(): void
|
||||||
|
{
|
||||||
|
// Unit tests have no et_pb_is_allowed(): a missing Divi function never means "allowed".
|
||||||
|
self::assertFalse((new ThemeOptionsAdapter())->canCustomize());
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @runInSeparateProcess */
|
||||||
|
public function testCanCustomizeAsksTheThemeCustomizerRight(): void
|
||||||
|
{
|
||||||
|
Functions\when('et_pb_is_allowed')->alias(static fn (string $cap): bool => $cap === 'theme_customizer');
|
||||||
|
|
||||||
|
self::assertTrue((new ThemeOptionsAdapter())->canCustomize());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -6,8 +6,10 @@ namespace LIT\MCP\Tests\Unit\Integrations\Divi\Release;
|
|||||||
|
|
||||||
use LIT\MCP\Integrations\Divi\Design\DesignSystemReader;
|
use LIT\MCP\Integrations\Divi\Design\DesignSystemReader;
|
||||||
use LIT\MCP\Integrations\Divi\Design\DesignSystemWriter;
|
use LIT\MCP\Integrations\Divi\Design\DesignSystemWriter;
|
||||||
|
use LIT\MCP\Integrations\Divi\Design\FontCatalog;
|
||||||
use LIT\MCP\Integrations\Divi\Design\GlobalColorsAdapter;
|
use LIT\MCP\Integrations\Divi\Design\GlobalColorsAdapter;
|
||||||
use LIT\MCP\Integrations\Divi\Design\GlobalVariablesAdapter;
|
use LIT\MCP\Integrations\Divi\Design\GlobalVariablesAdapter;
|
||||||
|
use LIT\MCP\Integrations\Divi\Design\ThemeOptionsAdapter;
|
||||||
use LIT\MCP\Integrations\Divi\Library\DiviLibraryStore;
|
use LIT\MCP\Integrations\Divi\Library\DiviLibraryStore;
|
||||||
use LIT\MCP\Integrations\Divi\Library\DiviRoles;
|
use LIT\MCP\Integrations\Divi\Library\DiviRoles;
|
||||||
use LIT\MCP\Integrations\Divi\Pages\DiviPages;
|
use LIT\MCP\Integrations\Divi\Pages\DiviPages;
|
||||||
@@ -82,6 +84,26 @@ final class DiviFeatureAssignmentTest extends TestCase
|
|||||||
'GlobalData::set_global_variables()' => ['variable-write'],
|
'GlobalData::set_global_variables()' => ['variable-write'],
|
||||||
'et_pb_is_allowed()' => ['variable-write'],
|
'et_pb_is_allowed()' => ['variable-write'],
|
||||||
],
|
],
|
||||||
|
ThemeOptionsAdapter::class => [
|
||||||
|
'et_sanitize_font_choices()' => ['theme-option-write'],
|
||||||
|
'et_sanitize_global_font_weight()' => ['theme-option-write'],
|
||||||
|
'et_sanitize_float_number()' => ['theme-option-write'],
|
||||||
|
'et_sanitize_int_number()' => ['theme-option-write'],
|
||||||
|
'et_divi_global_font_style()' => ['theme-option-write'],
|
||||||
|
'et_update_option()' => ['theme-option-write'],
|
||||||
|
'et_delete_option()' => ['theme-option-write'],
|
||||||
|
'ET_Core_PageResource::remove_static_resources()' => ['theme-option-write'],
|
||||||
|
'et_pb_force_regenerate_templates()' => ['theme-option-write'],
|
||||||
|
'et_pb_is_allowed()' => ['theme-option-write'],
|
||||||
|
'Nachbau et_divi_customizer_theme_settings()' => ['theme-option-read', 'theme-option-write'],
|
||||||
|
],
|
||||||
|
FontCatalog::class => [
|
||||||
|
'et_builder_get_fonts()' => ['font-upload', 'theme-option-write', 'variable-write'],
|
||||||
|
'et_builder_get_custom_fonts()' => ['font-upload', 'theme-option-write', 'variable-write'],
|
||||||
|
'et_builder_get_google_fonts()' => ['font-upload', 'theme-option-write', 'variable-write'],
|
||||||
|
'et_builder_get_websafe_fonts()' => ['font-upload', 'theme-option-write', 'variable-write'],
|
||||||
|
'et_pb_is_allowed()' => ['font-upload'],
|
||||||
|
],
|
||||||
DiviModules::class => [
|
DiviModules::class => [
|
||||||
'ModuleRegistration::get_all_core_modules_metadata()' => ['catalog'],
|
'ModuleRegistration::get_all_core_modules_metadata()' => ['catalog'],
|
||||||
'ModuleRegistration::get_default_attrs()' => ['catalog'],
|
'ModuleRegistration::get_default_attrs()' => ['catalog'],
|
||||||
@@ -143,7 +165,7 @@ final class DiviFeatureAssignmentTest extends TestCase
|
|||||||
}
|
}
|
||||||
|
|
||||||
self::assertSame(self::EXPECTED, $actual);
|
self::assertSame(self::EXPECTED, $actual);
|
||||||
self::assertSame(array_keys(self::EXPECTED), DiviAdapters::CLASSES, 'all ten adapters, in report order');
|
self::assertSame(array_keys(self::EXPECTED), DiviAdapters::CLASSES, 'all twelve adapters, in report order');
|
||||||
}
|
}
|
||||||
|
|
||||||
public function testReadingAdaptersCarryNoWriteFeature(): void
|
public function testReadingAdaptersCarryNoWriteFeature(): void
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ use PHPUnit\Framework\TestCase;
|
|||||||
|
|
||||||
final class DiviFeatureTest extends TestCase
|
final class DiviFeatureTest extends TestCase
|
||||||
{
|
{
|
||||||
public function testTheThirteenFeaturesAndTheirValues(): void
|
public function testTheSixteenFeaturesAndTheirValues(): void
|
||||||
{
|
{
|
||||||
self::assertSame(
|
self::assertSame(
|
||||||
[
|
[
|
||||||
@@ -21,6 +21,9 @@ final class DiviFeatureTest extends TestCase
|
|||||||
'PresetWrite' => 'preset-write',
|
'PresetWrite' => 'preset-write',
|
||||||
'ColorWrite' => 'color-write',
|
'ColorWrite' => 'color-write',
|
||||||
'VariableWrite' => 'variable-write',
|
'VariableWrite' => 'variable-write',
|
||||||
|
'ThemeOptionRead' => 'theme-option-read',
|
||||||
|
'ThemeOptionWrite' => 'theme-option-write',
|
||||||
|
'FontUpload' => 'font-upload',
|
||||||
'LibraryRead' => 'library-read',
|
'LibraryRead' => 'library-read',
|
||||||
'LibraryWrite' => 'library-write',
|
'LibraryWrite' => 'library-write',
|
||||||
'Roles' => 'roles',
|
'Roles' => 'roles',
|
||||||
@@ -34,7 +37,7 @@ final class DiviFeatureTest extends TestCase
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function testOnlyTheSixWriteFeaturesWrite(): void
|
public function testOnlyTheEightWriteFeaturesWrite(): void
|
||||||
{
|
{
|
||||||
$writing = array_values(array_map(
|
$writing = array_values(array_map(
|
||||||
static fn (DiviFeature $feature): string => $feature->value,
|
static fn (DiviFeature $feature): string => $feature->value,
|
||||||
@@ -42,7 +45,10 @@ final class DiviFeatureTest extends TestCase
|
|||||||
));
|
));
|
||||||
|
|
||||||
self::assertSame(
|
self::assertSame(
|
||||||
['page-write', 'preset-write', 'color-write', 'variable-write', 'library-write', 'tb-write'],
|
[
|
||||||
|
'page-write', 'preset-write', 'color-write', 'variable-write', 'theme-option-write', 'font-upload',
|
||||||
|
'library-write', 'tb-write',
|
||||||
|
],
|
||||||
$writing
|
$writing
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -56,6 +62,9 @@ final class DiviFeatureTest extends TestCase
|
|||||||
self::assertSame('Theme Builder lesen', DiviFeature::ThemeBuilderRead->label());
|
self::assertSame('Theme Builder lesen', DiviFeature::ThemeBuilderRead->label());
|
||||||
self::assertSame('Farben schreiben', DiviFeature::ColorWrite->label());
|
self::assertSame('Farben schreiben', DiviFeature::ColorWrite->label());
|
||||||
self::assertSame('Variablen schreiben', DiviFeature::VariableWrite->label());
|
self::assertSame('Variablen schreiben', DiviFeature::VariableWrite->label());
|
||||||
|
self::assertSame('Theme-Optionen lesen', DiviFeature::ThemeOptionRead->label());
|
||||||
|
self::assertSame('Theme-Optionen schreiben', DiviFeature::ThemeOptionWrite->label());
|
||||||
|
self::assertSame('Schriften hochladen', DiviFeature::FontUpload->label());
|
||||||
foreach ($labels as $label) {
|
foreach ($labels as $label) {
|
||||||
self::assertNotSame('', trim($label));
|
self::assertNotSame('', trim($label));
|
||||||
}
|
}
|
||||||
@@ -64,6 +73,7 @@ final class DiviFeatureTest extends TestCase
|
|||||||
public function testValuesResolveBackToTheFeature(): void
|
public function testValuesResolveBackToTheFeature(): void
|
||||||
{
|
{
|
||||||
self::assertSame(DiviFeature::ThemeBuilderWrite, DiviFeature::from('tb-write'));
|
self::assertSame(DiviFeature::ThemeBuilderWrite, DiviFeature::from('tb-write'));
|
||||||
|
self::assertSame(DiviFeature::ThemeOptionWrite, DiviFeature::from('theme-option-write'));
|
||||||
self::assertNull(DiviFeature::tryFrom('page'));
|
self::assertNull(DiviFeature::tryFrom('page'));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user