store; } public function roles(): DiviRoles { return $this->roles; } public function canUse(): ?WP_Error { if (! current_user_can('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'); } /** Whether Theme Builder objects exist for this connection: group not disabled and canUse(). */ public function visible(): bool { return ($this->visible === null || ($this->visible)()) && $this->canUse() === null; } /** * The Theme Builder access of a global library edit (GlobalUsages, ruling Q17 M1, review I-2): visible = Theme * Builder posts may be named (visible()); usable = a global edit may change them – the rights of every Theme * Builder write (canUse(), part of visible()) and a writable group, as the Divi tools on layouts. * * @return array{visible: bool, usable: bool} */ public function usageState(): array { $visible = $this->visible(); return ['visible' => $visible, 'usable' => $visible && $this->writableGate() === null]; } public function writableGate(): ?WP_Error { if ($this->writable === null || ($this->writable)()) { return null; } return Errors::forbidden( 'Schreiben im Divi Theme Builder ist für diese Verbindung abgeschaltet (Gruppe „Divi Theme Builder“ auf ' . '„nur lesen“ oder deaktiviert).', 'Einen Administrator fragen: Einstellungen → LIT MCP → Tools.' ); } /** * Layout $postId for the Divi tools (E1): null when it is no Theme Builder layout (the caller goes on); * like a missing ID without visible(); forbidden without edit_post; for writing also the group gate and * a published layout (Divi's TB layouts are always published). */ public function layout(mixed $postId, bool $forWrite): WP_Post|WP_Error|null { $id = PostGuard::id($postId); $post = $id === null ? null : get_post($id); if (! $post instanceof WP_Post || DiviThemeBuilder::areaOf((string) $post->post_type) === null) { return null; } $id = (int) $post->ID; if (! $this->visible()) { return PostGuard::notFound($id); } if (! current_user_can('edit_post', $id)) { return Errors::forbidden(sprintf('Keine Berechtigung, Theme-Builder-Layout %d zu bearbeiten.', $id)); } if (! $forWrite) { return $post; } $gate = $this->writableGate(); if ($gate !== null) { return $gate; } if ((string) $post->post_status !== 'publish') { return Errors::invalidInput( sprintf( 'Theme-Builder-Layout %d ist nicht aktiv (Status „%s“); nichts wurde gespeichert.', $id, (string) $post->post_status ), ['post_id' => 'nur aktive Theme-Builder-Layouts'] ); } return $post; } /** * Template $templateId for the Theme Builder tools. Any other ID, a hidden Theme Builder and a template the * user may not edit (canEditTemplate) answer identically: not found (ruling Q18, P12 – like tb-list, Q8/B11). */ public function template(mixed $templateId, bool $forWrite): WP_Post|WP_Error { $id = PostGuard::id($templateId); if ($id === null) { return self::invalidTemplateId(); } $post = get_post($id); if ( ! $post instanceof WP_Post || $post->post_type !== DiviThemeBuilder::TEMPLATE || (string) $post->post_status !== 'publish' || ! $this->canEditTemplate($id) ) { return Errors::notFound( sprintf('Theme-Builder-Vorlage %d gibt es nicht.', $id), 'Vorlagen mit lit/tb-list auflisten.' ); } return $forWrite ? ($this->writableGate() ?? $post) : $post; } /** The answer for a template_id that is missing or no ID (template(), lit/tb-list – review M-5). */ public static function invalidTemplateId(): WP_Error { return Errors::invalidInput('template_id fehlt oder ist keine ID.', ['template_id' => 'ID aus lit/tb-list']); } /** * A template the Theme Builder tools may change (lit/tb-assign, lit/tb-activate): rights and group gate * (template(), a template the user may not edit reads as missing – ruling Q18), no Theme Builder draft (E7), * never the default template (E8), the current template_rev. $offSwitch (lit/tb-activate with enabled: false, * ruling Q16): also a template visitors see although it is not published (e.g. in the trash – Divi renders * every attached, enabled template, B8), so it can always be switched off; any other unpublished template * still reads as missing. The off-switch also works while the Theme Builder has unsaved changes (ruling Q20). * * @param array $input template_id, template_rev */ public function changeableTemplate(array $input, bool $offSwitch = false): Template|WP_Error { // Review M-9: without the Theme Builder rights refuse like tb-list/tb-create-template – independent of the ID. $refusal = $this->canUse(); if ($refusal !== null) { return $refusal; } $post = $this->template($input['template_id'] ?? null, true); if ($post instanceof WP_Error) { $id = $offSwitch && Errors::codeOf($post) === 'not_found' ? $this->liveUnpublished($input['template_id'] ?? null) : null; if ($id === null) { return $post; } $gate = $this->writableGate(); return $gate ?? $this->checkedTemplate($id, $input, true); } return $this->checkedTemplate((int) $post->ID, $input, $offSwitch); } /** * The templates of the live Theme Builder post split by canEditTemplate() (ruling Q21 – one place for * tb-list and the dry runs of tb-assign/tb-activate): only "templates" may be shown; "hidden" serves counts * ("weitere aktive Vorlagen ohne Berechtigung"), never its IDs, titles or conditions (Q8/B11, P12). * * @return array{templates: list