'object', 'properties' => [ 'css' => ['type' => 'string', 'description' => 'Neues vollständiges CSS; "" leert es.'], 'append' => ['type' => 'string', 'description' => 'CSS, das ans Ende angehängt wird.'], 'replace' => [ 'type' => 'object', 'properties' => [ 'old' => ['type' => 'string', 'minLength' => 1, 'description' => 'Genau einmal im CSS.'], 'new' => ['type' => 'string'], ], 'required' => ['old', 'new'], 'additionalProperties' => false, ], ] + DesignChange::inputProperties(), 'additionalProperties' => false, ]; } public function outputSchema(): ?array { return DesignChange::outputSchema(); } /** It overwrites the stored text, and a client treating it as additive would run it without asking. */ public function annotations(): array { return Annotations::destructive(true); } public function isDirect(): bool { return false; } public function diviFeatures(array $input): array { return [DiviFeature::ThemeOptionRead, DiviFeature::ThemeOptionWrite]; } public function permission(array $input): bool|WP_Error { return DesignRights::customCss($this->options) ?? true; } public function execute(array $input): mixed { $refusal = DesignRights::customCss($this->options); if ($refusal !== null) { return $refusal; } $given = array_values(array_filter( self::MODES, static fn (string $mode): bool => array_key_exists($mode, $input) )); if (count($given) !== 1) { return Errors::invalidInput( 'Genau eines von css, append oder replace angeben; nichts wurde gespeichert.', ['css' => 'css, append oder replace – nicht keins und nicht mehrere'] ); } return $this->change->run( $this->name(), new CustomCssArea($this->store), $input, fn (array $raw): DesignPlan|WP_Error => $this->plan($raw, $given[0], $input[$given[0]]) ); } /** @param array $raw */ private function plan(array $raw, string $mode, mixed $value): DesignPlan|WP_Error { $before = (string) ($raw['css'] ?? ''); $after = $this->applied($before, $mode, $value); if ($after instanceof WP_Error) { return $after; } $verdict = $this->guard->check($after, $before); if ($verdict['errors'] !== []) { return Errors::invalidInput( 'Das CSS wird nicht gespeichert: ' . implode('; ', $verdict['errors']) . '.', [$mode => implode('; ', $verdict['errors'])] ); } $warnings = [CustomCssArea::SITE_WIDE]; if ($after === '' && $before !== '') { $warnings[] = 'Das gesamte eigene CSS der Website wird geleert.'; } return new DesignPlan( ['stylesheet' => (string) ($raw['stylesheet'] ?? ''), 'css' => $after], CustomCssDiff::between($before, $after), CustomCssDiff::summary($before, $after), null, array_merge($warnings, $verdict['warnings']) ); } private function applied(string $before, string $mode, mixed $value): string|WP_Error { if ($mode === 'replace') { return $this->replaced($before, $value); } if (! is_string($value)) { return Errors::invalidInput(sprintf('%s muss Text sein.', $mode), [$mode => 'Text']); } if ($mode === 'css') { return $value; } // append: the new rules start on a line of their own. return $before === '' || str_ends_with($before, "\n") ? $before . $value : $before . "\n" . $value; } private function replaced(string $before, mixed $value): string|WP_Error { $old = is_array($value) ? ($value['old'] ?? null) : null; $new = is_array($value) ? ($value['new'] ?? null) : null; if (! is_string($old) || $old === '' || ! is_string($new)) { return Errors::invalidInput('replace braucht old (nicht leer) und new.', ['replace' => '{old, new}']); } $count = substr_count($before, $old); if ($count === 0) { return Errors::invalidInput( 'replace.old kommt im CSS nicht vor; nichts wurde gespeichert.', ['replace.old' => 'Text genau wie im CSS'] ); } if ($count > 1) { return Errors::invalidInput( sprintf('replace.old kommt %d-mal vor und muss eindeutig sein; nichts wurde gespeichert.', $count), ['replace.old' => 'mehr Umgebung angeben, bis er einmal vorkommt'] ); } return str_replace($old, $new, $before); } }