Every match carries replaceable: true only when it is identical and in a
page or post, the scope lit/library-make-global replaces; the description
and the guide say so. An excluded post type is answered like any
unavailable one, by its name (PostGuard::unavailableType()), and the
excluded list takes the Theme Builder types from DiviThemeBuilder::postTypes().
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01At3V6SjwbMe6eMzVGW3Hs7
WpLinkTargets answers the library, internal "_" types and every type
without a Divi post_link_url option like a missing ID; replace-text strips
library IDs only from global subtrees.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Outline, node-get, write outlines, the render summary and replace-text's
opaque-node match name no library ID unless the library group is visible
and the user has divi_library; read-only still names them.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Reuses NewDraft for insert and rollback (P6), requires edit_global_library
for scope global (P7), refuses opaque and global roots before wrapping (P11)
and checks the node with StoredNodeGuard. The source page is never written.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Control characters removed by kses (wp_kses_no_null: C0, \\0 sequences)
or ControlChars (DEL) could hide a divi/* delimiter or an et_pb shortcode
from the raw-input check. The shared DiviMarkup helper checks the raw
and the normalized form and now guards every raw-content path:
content-create/-update content and media-update description (attachment
post_content). MediaImporter logs import failures with scheme, host,
port and path only; uninstall.php lists the removed user meta.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01At3V6SjwbMe6eMzVGW3Hs7
- m-1: the no-change Divi result carries preview_url/edit_url too
- m-2: overview names replace-text's per-page tree_rev
- m-3: divi-shortform documents validation_failed vs not_found for unknown IDs
- m-4: DiviWriter/DiviPageLoader final (no mocks left), neutral NewDraft
log detail, snapshots are Etappe 4
- m-5: Tested up to 7.1, changelog completed
- m-6: content-create warns when the extras changed the stored layout
- m-7: deleting a whole section outlines nothing instead of the next one
- m-8: uninstall removes the user meta lit_mcp_log_per_page; readme lists it
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01At3V6SjwbMe6eMzVGW3Hs7
Every file under src/ exits outside WordPress (defined('ABSPATH') ||
exit; after the use block, so a direct request no longer prints a fatal
error with the server path); divi-convert-fixture.php gets the WP_CLI
guard the other integration scripts have. A unit test keeps both true;
PSR1 side-effects warning excluded for the guard.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01At3V6SjwbMe6eMzVGW3Hs7
PodsFieldSource maps hidden, read_only, admin_only and
restrict_role/-capability like Pods' own view: hidden -> new status
HIDDEN, read_only -> READONLY, restricted and not permitted -> READONLY
with read_only_restricted, else HIDDEN. HIDDEN fields are not described,
read out, queried or written (they look unknown) but stay in the schema
for the media usage search; write() re-checks before Pods' save().
Required-but-restricted fields no longer block a create. Pods is not
active on DDEV: covered by unit tests with a field-option fixture.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01At3V6SjwbMe6eMzVGW3Hs7
DiviPage::builderUrl() is shared by DiviPage::editUrl() and the content
composer, which now returns edit_url; content-create without divi keeps
the wp-admin link. divi-write-smoke checks the builder URL and, new in
section 9, the M1/M2 gates live (raw Divi markup refused; Divi read-only
stops content-create divi and execute-ability lit/divi-insert).
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01At3V6SjwbMe6eMzVGW3Hs7
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
lit/content-create and lit/content-update refuse content containing the
divi/* block delimiters WordPress' block parser accepts or Divi 4
shortcodes, so the builder-only gates (S7/S26) can't be bypassed. Other
block markup stays allowed (normal kses).
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
MenuWriter::fail guards wp_delete_post for created items: the touched items
are still restored and the client gets partial_write (with previous_items)
instead of internal_error.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01At3V6SjwbMe6eMzVGW3Hs7
PostChangeResult is PostSaveFollowUp (the after-save pipeline, not a
result); DiviChange::changed() is record() (moved nodes are recorded but
not touched). cleanValue/withoutControls were renamed in wave 2.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01At3V6SjwbMe6eMzVGW3Hs7
Integrations name their own admin group (Integration::label, registry
labels()); ToolsTab no longer hard-codes ids and explains why it re-applies
withGroups. AccessTab and LogTab share AdminPostAction (guard, redirect,
terminate seam). ExposurePolicy parses option keys in one place, has one
isRequired() check and a clear $groups docblock (null vs []).
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01At3V6SjwbMe6eMzVGW3Hs7
The SQL prefilter and the per-page hit computation move to DiviTextSearch;
DiviPageLoader::loadWritable serves DiviWriter::page and the dry run;
PostStatuses::NOT_CONTENT replaces the duplicated status list (same set,
order in the SQL list only). The private log() wrapper is inlined.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01At3V6SjwbMe6eMzVGW3Hs7
NewDraft owns insert (partial write on a throwing hook), refuse and discard (via
Rollback::attempt and WriteFailed::*) and the partial logging; ContentCreateAbility
keeps checkDivi/prepare/combined (Errors::fieldsOf) and the result. Both discard
callers still pass REASON_EXTRAS. Behaviour unchanged; NewDraftTest adds the
throwing-delete branch.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
- PostRow::update(id, columns, failure) / ::save(): the slashed wp_update_post of
internal row writes, refusals as RuntimeException '<failure>: <why>'. Used by
ContentUpdate::restoreRow, PostCloner::updateRow, MediaMetaWriter apply/restore.
- PostGuard::assertNoMissedSchedule(postId, message) replaces the re-read + check in
ContentUpdate::restoreRow, PostCloner and DiviSaver::restore (DiviSaver keeps its
own kses window; M4).
- PostCloner::restore uses Rollback::attempt.
Log messages unchanged.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
- WriteFailed::rolledBack()/partial() replace the 11-parameter constructor (now
private); the hint/state of either error are plain $hint/$state.
- Rollback::attempt(...steps) runs every undo step and collects failure messages;
used by ContentCreate::discard, ContentUpdate::rollBack, MediaUpdate::rollBack and
DiviSaver::restore.
- Errors::clientMessage/codeOf/fieldsOf replace DiviReplaceTextAbility::reason,
the JSON digging in MediaUpdateAbility::failure and ContentCreateAbility::fields.
- Errors docblock documents the exception/WP_Error split.
- ContentCreateAbility::discard takes the client reason as a parameter (both callers
still pass REASON_EXTRAS; changing the Divi text needs a ruling).
Error payloads unchanged.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The three lazy db() methods use the $this->db ?? $GLOBALS['wpdb'] one-liner that
SampledMetaSource, MediaUsageFinder and ConnectionRepository already use.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
- Hints (PostPresenter::DIVI_NOTE, outline, catalog) and server instructions (tree_rev rule)
name the Divi write tools; guides divi-shortform and divi-placeholders, additions to
divi-structure, divi-attrs, overview (tool groups, 'nur lesen'), workflows, content-types.
- D7 wording and the S7 exception in readme, guides and spec 1.3/5.2; readme 0.3.0.
- ToolNamingTest: every referenced guide topic exists; overview lists exactly the topics.
- tests/Integration/divi-write-smoke.sh: all write tools via OAuth, editor without
unfiltered_html with benign probes (S6), Divi's own save filters on the new fixture
synthetic-divi-filters.html (S7); admin-render.php accepts the new wrap class.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01At3V6SjwbMe6eMzVGW3Hs7
New input divi (1-30 sections, short form like lit-divi-insert) builds a Divi 5
layout in one divi/placeholder. The whole description is checked before the
insert (Divi's map for "no entry yet"); the draft is inserted with its normal
WordPress fields only, then DiviContentComposer composes again bound to the new
entry and stores it via DiviSaver::saveNew (same kses window as save(), strict
DiviFlags). A refusal or failure deletes the draft. Result adds tree_rev and the
outline.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Review fix round 1 for lit/page-render-summary: extract shared Text::shorten()
(reused by NodeSummary), replace data: URIs with a short marker, prefer
data-src/data-lazy-src over placeholder src, dedup nested p/li/blockquote
text and ignore script/style/noscript content, restore every setup_postdata
global (not just $post), drain output buffers robustly around the_content,
and disclose that rendering can trigger oEmbed/network requests.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01At3V6SjwbMe6eMzVGW3Hs7
Task 14 (rulings P17/P18, Q10):
- lit/menu-get returns menu_rev (Rev::ofMenu over id, parent, position,
title, url, object, object_id); lit/menu-set-items requires it, refuses
a stale one with rev_mismatch (hint: lit/menu-get) and returns the new
menu_rev. Errors::revMismatch takes an optional message/hint.
- GuideRepository: filter lit_mcp_guides adds/overrides topics; invalid
topic names and non-string content are ignored, files stay the default.
- lit/post-trash names front page, posts page and linking menus in the
confirmation and as warnings; lit/menu-assign-location needs confirm
to remove an assigned menu and names it; lit/post-schedule warns that
a private entry becomes public; Divi hints no longer point to
non-existing lit-divi-* tools.
- PostGuard::missedSchedule: 60 s publish window, zero GMT date falls
back to get_gmt_from_date(post_date); post-schedule refuses dates
within that window.
- write_failed after a clean rollback that keeps a revision/backup says
"Die Aenderung wurde nicht gespeichert (... bleibt erhalten)" via the
new state parameter (Errors::writeFailed / WriteFailed rolledBackState).
- wp-basis-smoke: menu_rev flow, unassign confirm, trash/menu and
private-schedule warnings.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01At3V6SjwbMe6eMzVGW3Hs7
Fix round 1 (task 10 review):
- FieldValidator's "unknown field" validation error (reaches the
client via validation_failed's fields) and PostPresenter's
DIVI_HINT (surfaced in the result's hints array) still named
lit-content-types/lit-content-update as plain lit-<name>; converted
both to the neutral "lit/<name> (direkt: lit-<name>)" form like the
other hints from task 10. Re-grepped src/ for any other client-facing
error/field/hints text naming a direct-by-default tool as lit-<name>
(descriptions/schemas excluded per Q11c) - none left.
- Added Limits::bounds() (field => [min, max]) and made LimitsTab use
it for the number inputs' min/max instead of a second, hardcoded
copy of the same bounds.
- LimitsTab now shows a description note when the configured upload_mb
exceeds wp_max_upload_size() (PHP's upload_max_filesize/post_max_size),
since WordPress would refuse a larger upload before this limit ever
applies.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01At3V6SjwbMe6eMzVGW3Hs7
Adds Einstellungen → LIT MCP → Tools (direct/indirect/disabled per
ability, only ExposurePolicy-allowed modes offered) and → Limits
(upload size, log retention, snapshot count), wired into Plugin.php.
Ruling Q11b (carried from task 6): overview.md notes that direct
visibility is configurable; hints in src/ that name a tool which is
direct by default but can be switched to indirect (content-get,
content-query, content-types, post-publish, media-search) now use the
neutral "lit/<name> (direkt: lit-<name>)" form so they stay correct
after an admin override. lit/guide (always direct) and the
media-preview/-download DIRECT_ONLY tools are unaffected.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01At3V6SjwbMe6eMzVGW3Hs7
I1: query the attachments themselves author-restricted when the user
lacks edit_others_posts (PostGuard::ownPostsOnly, extracted from
ContentQueryAbility so content-query reuses the exact same rule)
instead of relying solely on a post-query editable() filter, which
could leak the existence of other users' attachments via an empty
page plus next_cursor. editable() stays as a backstop.
Also: M1 document that only status=inherit attachments are listed;
M2 tie-break ordering by ID (content-query and media-search) for
stable pagination; M3 no next_cursor once the last candidate was
examined; M4 reject an explicit non-image mime with missing_alt;
M5 additional coverage (posts_per_page, cursor offset pass-through,
invalid cursor, post_status/date/mime args); mime maxLength 100.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01At3V6SjwbMe6eMzVGW3Hs7
Usage search distinguishes certain (ID referenced) and possible (file URL
only) usages, hides titles the user cannot read, and keeps queries bounded.
MediaGuard checks per-attachment capabilities and refuses trashed media
unless the caller opts in.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01At3V6SjwbMe6eMzVGW3Hs7
Indirect abilities are written lit/<name> (via mcp-adapter-execute-ability) in
guides, descriptions and hints, direct tools lit-<name>; guides use the
client-visible codes (write_failed, partial_write) and say exactly which tools
return a revision_id. ToolNamingTest keeps guides and sources consistent with
Ability::isDirect() and checks the overview's direct-tool list.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01At3V6SjwbMe6eMzVGW3Hs7
- isStored() compares the stored option with sanitize_option() of the new value
(core esc_html()s blogname/blogdescription): setting "Müller & Söhne" again
no longer looks like a failed write; lit/wp-settings-get and previous return
those two decoded, as the user would type them.
- The change log runs as FollowUp (warning on failure); a failed write throws
WriteFailed (write_failed / partial_write with keys), partial writes are
logged with result partial.
- permalink_structure needs a real %tag% (WordPress' own /%[^/%]+%/ rule).
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01At3V6SjwbMe6eMzVGW3Hs7
Fields from the meta/sample sources need edit_post_meta (update, checked in
PostWriter::prepare) or add_post_meta (create, checked right after the insert;
a refusal deletes the new draft again) for their key; a refusal is a
fields.<key> validation error.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01At3V6SjwbMe6eMzVGW3Hs7
publish/unpublish/schedule/trash run their WordPress call through StatusWrite:
a throwing hook becomes WriteFailed (rollback incomplete, with post_id) and is
logged with result partial instead of surfacing as internal_error.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01At3V6SjwbMe6eMzVGW3Hs7
after_save hook, re-read, change log and ToolResult (rev, log_id, warnings)
were repeated in nine post abilities; they now share one helper. Behaviour
and warning texts are unchanged.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01At3V6SjwbMe6eMzVGW3Hs7