fix(security): content-create divi follows the Divi group switches (audit M2)
The divi input of lit/content-create is refused with unsupported when lit/divi-insert is disabled or the Divi group is disabled or read-only (ExposurePolicy::allows as a cross-group gate, asked at execute time). content-create without divi keeps working. Adds a registrar test that read-only write tools are never registered, so execute-ability can't reach them either. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01At3V6SjwbMe6eMzVGW3Hs7
This commit is contained in:
@@ -56,10 +56,11 @@ final class ContentCreateDiviTest extends TestCase
|
||||
}
|
||||
|
||||
/** @return ContentComposer&MockInterface */
|
||||
private function composer(bool $available = true): ContentComposer
|
||||
private function composer(bool $available = true, bool $writable = true): ContentComposer
|
||||
{
|
||||
$composer = Mockery::mock(ContentComposer::class);
|
||||
$composer->allows('available')->andReturn($available);
|
||||
$composer->allows('writable')->andReturn($writable);
|
||||
$composer->allows('check')->with('page', self::SECTIONS)->andReturn(null);
|
||||
|
||||
return $composer;
|
||||
@@ -139,11 +140,39 @@ final class ContentCreateDiviTest extends TestCase
|
||||
self::assertArrayHasKey('content', self::errorFields($both));
|
||||
}
|
||||
|
||||
/**
|
||||
* Security M2: with the Divi group disabled or "nur lesen", or lit/divi-insert disabled, the
|
||||
* divi input is refused before anything is created; content-create without divi keeps working.
|
||||
*/
|
||||
public function testDiviIsRefusedWhenDiviWritesAreSwitchedOff(): void
|
||||
{
|
||||
Functions\expect('wp_insert_post')->never();
|
||||
|
||||
$error = $this->ability($this->composer(true, false))->execute(self::INPUT);
|
||||
|
||||
$payload = self::assertToolError($error, 'unsupported');
|
||||
self::assertStringContainsString('Tools', (string) $payload['hint']);
|
||||
self::assertStringContainsString('ohne divi', (string) $payload['hint']);
|
||||
}
|
||||
|
||||
public function testWithoutDiviTheGateIsNotAsked(): void
|
||||
{
|
||||
$composer = Mockery::mock(ContentComposer::class);
|
||||
$composer->expects('writable')->never();
|
||||
Functions\when('wp_insert_post')->justReturn(42);
|
||||
$this->stubStoredDraft();
|
||||
|
||||
$result = $this->ability($composer)->execute(['post_type' => 'page', 'title' => 'Start']);
|
||||
|
||||
self::assertTrue($result['ok']);
|
||||
}
|
||||
|
||||
public function testEveryValidationErrorComesBackAndNothingIsCreated(): void
|
||||
{
|
||||
Functions\expect('wp_insert_post')->never();
|
||||
$composer = Mockery::mock(ContentComposer::class);
|
||||
$composer->allows('available')->andReturn(true);
|
||||
$composer->allows('writable')->andReturn(true);
|
||||
$composer->allows('check')->andReturn(Errors::invalidAttrPath(['divi[0].attrs.x' => 'unbekannt']));
|
||||
$writer = Mockery::mock(PostWriter::class);
|
||||
$writer->allows('prepare')->andReturn(Errors::invalidInput('ungültig', ['fields.role' => 'Pflichtfeld fehlt.']));
|
||||
@@ -238,6 +267,11 @@ final class ContentCreateDiviTest extends TestCase
|
||||
return true;
|
||||
}
|
||||
|
||||
public function writable(): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
public function check(string $postType, array $sections): ?WP_Error
|
||||
{
|
||||
return $this->real->check($postType, $sections);
|
||||
|
||||
Reference in New Issue
Block a user