fix: Theme Builder access needs edit_theme_options (ruling Q15)
Divi's Theme Builder page is gated by edit_theme_options; without it every TB ID answers like a missing one. Docblocks (Q16): the wp_die guard cannot restore WP_Hook nesting after a third-party wp_die inside a hook, and canEditTemplate expects IDs from Divi's list. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -334,6 +334,10 @@ class DiviThemeBuilder
|
|||||||
* Runs $call with every wp_die handler replaced by one that throws (F17/F18) and with its output
|
* Runs $call with every wp_die handler replaced by one that throws (F17/F18) and with its output
|
||||||
* discarded. Only this request's in-memory hooks change; the handler filters (added at PHP_INT_MAX
|
* discarded. Only this request's in-memory hooks change; the handler filters (added at PHP_INT_MAX
|
||||||
* with a closure of this call) and every output buffer opened meanwhile are removed on every path.
|
* with a closure of this call) and every output buffer opened meanwhile are removed on every path.
|
||||||
|
* Limit: when a third-party callback calls wp_die() inside a hook that Divi's wp_insert_post() fires,
|
||||||
|
* the exception leaves that WP_Hook mid-iteration; its nesting level and $wp_current_filter are not
|
||||||
|
* restored (WordPress offers no API for that), so later hooks of this request may see a stale filter
|
||||||
|
* stack. The request ends with the tool's error anyway; the check before Divi is the primary defense.
|
||||||
*/
|
*/
|
||||||
private function guarded(callable $call): mixed
|
private function guarded(callable $call): mixed
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -15,7 +15,8 @@ defined('ABSPATH') || exit;
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Rights on Theme Builder objects (spec §4.5, E3 – never laxer than Divi): the Theme Builder at all needs
|
* Rights on Theme Builder objects (spec §4.5, E3 – never laxer than Divi): the Theme Builder at all needs
|
||||||
* edit_others_posts (Divi's own TB requirement, F19/F23) and the Divi role option theme_builder (off for
|
* edit_others_posts (Divi's own TB requirement, F19/F23), edit_theme_options (the gate of Divi's Theme
|
||||||
|
* Builder page, theme-builder.php:2059-2063, ruling Q15) and the Divi role option theme_builder (off for
|
||||||
* every role but administrator by default, F12); one layout or template additionally edit_post on it.
|
* every role but administrator by default, F12); one layout or template additionally edit_post on it.
|
||||||
* Without those, or with the group "Divi Theme Builder" disabled, every TB ID answers like a missing ID
|
* Without those, or with the group "Divi Theme Builder" disabled, every TB ID answers like a missing ID
|
||||||
* (P12 pattern). The group's "nur lesen" stops writes (E9). Not final so abilities can be tested with a mock.
|
* (P12 pattern). The group's "nur lesen" stops writes (E9). Not final so abilities can be tested with a mock.
|
||||||
@@ -52,6 +53,9 @@ class ThemeBuilderAccess
|
|||||||
if (! current_user_can('edit_others_posts')) {
|
if (! current_user_can('edit_others_posts')) {
|
||||||
return Errors::forbidden('Keine Berechtigung für den Divi Theme Builder (edit_others_posts).');
|
return Errors::forbidden('Keine Berechtigung für den Divi Theme Builder (edit_others_posts).');
|
||||||
}
|
}
|
||||||
|
if (! current_user_can('edit_theme_options')) {
|
||||||
|
return Errors::forbidden('Keine Berechtigung für den Divi Theme Builder (edit_theme_options).');
|
||||||
|
}
|
||||||
|
|
||||||
return $this->roles->refusal('theme_builder');
|
return $this->roles->refusal('theme_builder');
|
||||||
}
|
}
|
||||||
@@ -151,7 +155,11 @@ class ThemeBuilderAccess
|
|||||||
return $forWrite ? ($this->writableGate() ?? $post) : $post;
|
return $forWrite ? ($this->writableGate() ?? $post) : $post;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Whether template $id may be named to this user (tb-list shows other IDs not at all, ruling Q8/B11). */
|
/**
|
||||||
|
* Whether template $id may be named to this user (tb-list shows other IDs not at all, ruling Q8/B11).
|
||||||
|
* $id must come from Divi's own template list (templateIds()/liveTemplates()/detachedIds()): this checks
|
||||||
|
* only the rights, not that $id is a template.
|
||||||
|
*/
|
||||||
public function canEditTemplate(int $id): bool
|
public function canEditTemplate(int $id): bool
|
||||||
{
|
{
|
||||||
return $this->visible() && current_user_can('edit_post', $id);
|
return $this->visible() && current_user_can('edit_post', $id);
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ declare(strict_types=1);
|
|||||||
namespace LIT\MCP\Tests\Unit\Integrations\Divi\ThemeBuilder;
|
namespace LIT\MCP\Tests\Unit\Integrations\Divi\ThemeBuilder;
|
||||||
|
|
||||||
use Brain\Monkey\Functions;
|
use Brain\Monkey\Functions;
|
||||||
|
use LIT\MCP\Integrations\WordPress\PostGuard;
|
||||||
use LIT\MCP\Tests\Support\LibraryFixtures;
|
use LIT\MCP\Tests\Support\LibraryFixtures;
|
||||||
use LIT\MCP\Tests\Support\ThemeBuilderFixtures;
|
use LIT\MCP\Tests\Support\ThemeBuilderFixtures;
|
||||||
use LIT\MCP\Tests\Unit\TestCase;
|
use LIT\MCP\Tests\Unit\TestCase;
|
||||||
@@ -31,6 +32,25 @@ final class ThemeBuilderAccessTest extends TestCase
|
|||||||
self::assertToolError(ThemeBuilderFixtures::access()->canUse(), 'forbidden');
|
self::assertToolError(ThemeBuilderFixtures::access()->canUse(), 'forbidden');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Ruling Q15: Divi's Theme Builder page also needs edit_theme_options (theme-builder.php:2059-2063). */
|
||||||
|
public function testWithoutEditThemeOptionsTheThemeBuilderIsHidden(): void
|
||||||
|
{
|
||||||
|
LibraryFixtures::given([ThemeBuilderFixtures::layout(1062), ThemeBuilderFixtures::templatePost()], [
|
||||||
|
'edit_theme_options',
|
||||||
|
]);
|
||||||
|
$access = ThemeBuilderFixtures::access();
|
||||||
|
|
||||||
|
self::assertToolError($access->canUse(), 'forbidden');
|
||||||
|
self::assertStringContainsString('edit_theme_options', self::clientError($access->canUse())['message']);
|
||||||
|
self::assertFalse($access->visible());
|
||||||
|
self::assertFalse($access->canEditTemplate(ThemeBuilderFixtures::TEMPLATE));
|
||||||
|
self::assertToolError($access->canCreate(), 'forbidden');
|
||||||
|
$hidden = $access->layout(1062, false);
|
||||||
|
self::assertSame(self::clientError($hidden), self::clientError(PostGuard::notFound(1062)));
|
||||||
|
$template = self::assertToolError($access->template(ThemeBuilderFixtures::TEMPLATE, false), 'not_found');
|
||||||
|
self::assertSame('Theme-Builder-Vorlage 1066 gibt es nicht.', $template['message'], 'like a missing ID');
|
||||||
|
}
|
||||||
|
|
||||||
/** P12 pattern: without access a layout ID answers exactly like a missing one. */
|
/** P12 pattern: without access a layout ID answers exactly like a missing one. */
|
||||||
public function testLayoutsAreHiddenLikeMissingIdsWithoutAccess(): void
|
public function testLayoutsAreHiddenLikeMissingIdsWithoutAccess(): void
|
||||||
{
|
{
|
||||||
|
|||||||
Reference in New Issue
Block a user