fix(security): no direct HTTP access to plugin PHP files (audit L2, cheap part)

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
This commit is contained in:
2026-09-26 19:32:12 +02:00
co-authored by Claude Opus 5.5
parent 4d4bf57c9f
commit b24eb6c3e9
185 changed files with 418 additions and 1 deletions
@@ -7,6 +7,8 @@ namespace LIT\MCP\Integrations\WordPress\Content;
use Throwable;
use WP_Error;
defined('ABSPATH') || exit;
/**
* Builds the content of a new entry for lit/content-create from a structured description
* (spec §6.2: "optional Divi-Inhalt in Kurzform"). Implemented by the Divi integration, so the
@@ -17,6 +17,8 @@ use Throwable;
use WP_Error;
use WP_Post;
defined('ABSPATH') || exit;
/**
* lit/content-create: new entry, always a draft. All or nothing (spec §8.2): input is
* validated before the insert; if fields, terms or the featured image cannot be written
@@ -10,6 +10,8 @@ use LIT\MCP\Integrations\WordPress\PostGuard;
use LIT\MCP\Integrations\WordPress\Schema\ContentSchema;
use WP_Error;
defined('ABSPATH') || exit;
final class ContentGetAbility implements Ability
{
public function __construct(private ContentSchema $schema, private PostPresenter $presenter)
@@ -14,6 +14,8 @@ use LIT\MCP\Integrations\WordPress\Schema\FieldDefinition;
use WP_Error;
use WP_Post;
defined('ABSPATH') || exit;
final class ContentQueryAbility implements Ability
{
private const STATUSES = ['publish', 'draft', 'pending', 'private', 'future'];
@@ -10,6 +10,8 @@ use LIT\MCP\Abilities\Errors;
use LIT\MCP\Integrations\WordPress\Schema\ContentSchema;
use WP_Error;
defined('ABSPATH') || exit;
final class ContentTypesAbility implements Ability
{
public function __construct(private ContentSchema $schema)
@@ -22,6 +22,8 @@ use Throwable;
use WP_Error;
use WP_Post;
defined('ABSPATH') || exit;
/**
* lit/content-update: changes an entry without touching its status. rev (from
* lit/content-get) is required so parallel edits are never overwritten (spec §2
@@ -12,6 +12,8 @@ use RuntimeException;
use Throwable;
use WP_Error;
defined('ABSPATH') || exit;
/**
* The transaction of lit/content-create: inserting the draft and every way back out of it.
* The three exits side by side:
@@ -11,6 +11,8 @@ use LIT\MCP\Integrations\WordPress\Schema\ContentSchema;
use LIT\MCP\Integrations\WordPress\Schema\FieldDefinition;
use WP_Post;
defined('ABSPATH') || exit;
/**
* Output format of an entry for the content tools. Not final so abilities can be
* unit-tested with a mock.
@@ -14,6 +14,8 @@ use Throwable;
use WP_Error;
use WP_Post;
defined('ABSPATH') || exit;
/**
* Shared validation and write logic of lit/content-create and lit/content-update.
* prepare() validates everything before the first write (all or nothing, spec §8.2);
@@ -12,6 +12,8 @@ use LIT\MCP\Abilities\ToolResult;
use LIT\MCP\ChangeLog\ChangeLog;
use WP_Error;
defined('ABSPATH') || exit;
/**
* lit/menu-assign-location: assigns a menu to a theme location (menu_id 0 removes
* the assignment – the menu then disappears from the site, so removing an existing
@@ -13,6 +13,8 @@ use LIT\MCP\Abilities\ToolResult;
use LIT\MCP\ChangeLog\ChangeLog;
use WP_Error;
defined('ABSPATH') || exit;
/**
* lit/menu-create: creates an empty menu and optionally assigns it to a theme
* location. Menu items are added afterwards via lit/menu-set-items (Task 14).
@@ -10,6 +10,8 @@ use LIT\MCP\Abilities\Errors;
use LIT\MCP\Abilities\Rev;
use WP_Error;
defined('ABSPATH') || exit;
/**
* lit/menu-get: a menu as a tree (P4 ruling: same node shape lit/menu-set-items,
* Task 14, accepts as input, so the result can be edited and sent back as-is).
@@ -9,6 +9,8 @@ use LIT\MCP\Abilities\Annotations;
use LIT\MCP\Abilities\Errors;
use WP_Error;
defined('ABSPATH') || exit;
/**
* lit/menu-list: overview of all menus and the theme's menu locations, and which
* menu (if any) currently sits at each location.
@@ -4,6 +4,8 @@ declare(strict_types=1);
namespace LIT\MCP\Integrations\WordPress\Menus;
defined('ABSPATH') || exit;
/**
* Theme menu locations (theme mod nav_menu_locations).
*/
@@ -7,6 +7,8 @@ namespace LIT\MCP\Integrations\WordPress\Menus;
use LIT\MCP\Abilities\Errors;
use WP_Error;
defined('ABSPATH') || exit;
/**
* Applies add/move/remove operations to the current menu tree and returns the
* desired tree for MenuWriter – so single operations and the declarative mode
@@ -14,6 +14,8 @@ use LIT\MCP\Abilities\WriteFailed;
use LIT\MCP\ChangeLog\ChangeLog;
use WP_Error;
defined('ABSPATH') || exit;
/**
* lit/menu-set-items: sets a menu's items, declaratively (items = whole target tree) or
* as add/move/remove operations. Both run through one path: operations are applied to
@@ -4,6 +4,8 @@ declare(strict_types=1);
namespace LIT\MCP\Integrations\WordPress\Menus;
defined('ABSPATH') || exit;
/**
* Turns the flat wp_get_nav_menu_items() list into a nested tree. The node shape
* (node()) is deliberately the same shape lit/menu-set-items (Task 14) accepts as
@@ -10,6 +10,8 @@ use Throwable;
use WP_Post;
use WP_Term;
defined('ABSPATH') || exit;
/**
* Validates a desired menu tree against the current items and writes it.
*
+2
View File
@@ -10,6 +10,8 @@ use RuntimeException;
use WP_Error;
use WP_Post;
defined('ABSPATH') || exit;
/**
* Loads entries and post types for abilities, including the capability checks
* of spec §4.5. Every error is ready to be returned from permission()/execute().
+2
View File
@@ -7,6 +7,8 @@ namespace LIT\MCP\Integrations\WordPress;
use RuntimeException;
use WP_Error;
defined('ABSPATH') || exit;
/**
* Internal writes of post-row columns through wp_update_post() (slashed, WP_Error mode):
* rollbacks, clone rows, media metadata. Refusals become RuntimeExceptions with an internal
@@ -10,6 +10,8 @@ use LIT\MCP\Abilities\ToolResult;
use LIT\MCP\ChangeLog\ChangeLog;
use WP_Post;
defined('ABSPATH') || exit;
/**
* What every post tool does once its write is saved: fire lit_mcp_after_save, re-read the
* post, write the change log and build the ToolResult with the new rev. From here on a
@@ -4,6 +4,8 @@ declare(strict_types=1);
namespace LIT\MCP\Integrations\WordPress;
defined('ABSPATH') || exit;
/** Post statuses shared by the plugin's own SQL queries. */
final class PostStatuses
{
@@ -10,6 +10,8 @@ use LIT\MCP\Integrations\WordPress\PostGuard;
use LIT\MCP\Integrations\WordPress\Schema\ContentSchema;
use WP_Error;
defined('ABSPATH') || exit;
/**
* lit/page-render-summary: what a visitor sees in the content area – headings, texts, links,
* images with alt text and obvious problems (spec §5.7, §8.1). Works for every managed entry,
@@ -6,6 +6,8 @@ namespace LIT\MCP\Integrations\WordPress\Render;
use WP_Post;
defined('ABSPATH') || exit;
/**
* Renders the content area of an entry in-process, like the front end does for the_content
* (Divi 5 renders its blocks there – verified in DDEV). Header, footer and Theme Builder
@@ -10,6 +10,8 @@ use DOMNode;
use DOMXPath;
use LIT\MCP\Abilities\Text;
defined('ABSPATH') || exit;
/**
* Structured excerpt of rendered HTML (spec §5.7 lit/page-render-summary): headings, text
* blocks, links, images with alt text and the obvious problems. Every extracted string is
@@ -4,6 +4,8 @@ declare(strict_types=1);
namespace LIT\MCP\Integrations\WordPress;
defined('ABSPATH') || exit;
/**
* Restore point before a change (spec §2 principle 7): WordPress stores the
* current state as a revision (only if it differs from the newest one) and the
@@ -17,6 +17,8 @@ use LIT\MCP\Integrations\WordPress\Schema\ContentSchema;
use WP_Error;
use WP_Post;
defined('ABSPATH') || exit;
/**
* lit/revision-restore: restores title, content and excerpt of an entry from one of
* its own revisions; the status is not touched. Because this overwrites the current
@@ -13,6 +13,8 @@ use LIT\MCP\Integrations\WordPress\Schema\ContentSchema;
use WP_Error;
use WP_Post;
defined('ABSPATH') || exit;
/**
* lit/revisions-list: revisions of an entry, newest first. Autosaves are included
* (marked autosave: true) rather than filtered out, so the client can decide. Each
@@ -6,6 +6,8 @@ namespace LIT\MCP\Integrations\WordPress\Schema;
use LogicException;
defined('ABSPATH') || exit;
/**
* Detects the content model of this installation (spec §6.1): post types with a
* UI, their taxonomies and their fields merged from all field sources.
@@ -4,6 +4,8 @@ declare(strict_types=1);
namespace LIT\MCP\Integrations\WordPress\Schema;
defined('ABSPATH') || exit;
/**
* Description of one field of a post type, independent of where it comes from.
*/
@@ -6,6 +6,8 @@ namespace LIT\MCP\Integrations\WordPress\Schema;
use RuntimeException;
defined('ABSPATH') || exit;
/**
* A provider of field definitions for post types that can also read and write
* the values of its own fields (Pods via the Pods API, everything else via meta).
@@ -6,6 +6,8 @@ namespace LIT\MCP\Integrations\WordPress\Schema;
use DateTimeImmutable;
defined('ABSPATH') || exit;
/**
* Checks field values against the detected schema before anything is written.
*/
@@ -6,6 +6,8 @@ namespace LIT\MCP\Integrations\WordPress\Schema;
use RuntimeException;
defined('ABSPATH') || exit;
/**
* Shared read/write via the post meta API. write() throws when WordPress did not
* store a value, so callers can roll back (all or nothing, spec §8.2).
@@ -4,6 +4,8 @@ declare(strict_types=1);
namespace LIT\MCP\Integrations\WordPress\Schema;
defined('ABSPATH') || exit;
/**
* Fields registered with register_meta()/register_post_meta().
*/
@@ -4,6 +4,8 @@ declare(strict_types=1);
namespace LIT\MCP\Integrations\WordPress\Schema;
defined('ABSPATH') || exit;
/**
* Meta keys actually used by the most recent posts of a type. Keys starting with
* "_" are internal and only shown when another source knows them.
@@ -7,6 +7,8 @@ namespace LIT\MCP\Integrations\WordPress\Schema;
use RuntimeException;
use WP_Term;
defined('ABSPATH') || exit;
/**
* Resolves term input ({taxonomy: [id | name, …]}) and replaces a post's terms.
* resolve() never writes, so a whole call can be validated first (all or nothing).
@@ -6,6 +6,8 @@ namespace LIT\MCP\Integrations\WordPress\Settings;
use WP_Post;
defined('ABSPATH') || exit;
/**
* The WordPress options LIT MCP may read and write (spec §6.5), with validation.
*
@@ -9,6 +9,8 @@ use LIT\MCP\Abilities\Annotations;
use LIT\MCP\Abilities\Errors;
use WP_Error;
defined('ABSPATH') || exit;
final class WpSettingsGetAbility implements Ability
{
public function __construct(private SettingsWhitelist $whitelist)
@@ -16,6 +16,8 @@ use RuntimeException;
use Throwable;
use WP_Error;
defined('ABSPATH') || exit;
/**
* lit/wp-settings-update: writes whitelisted WordPress options, all or nothing.
*
@@ -13,6 +13,8 @@ use Throwable;
use WP_Error;
use WP_Post;
defined('ABSPATH') || exit;
/**
* Copies entries (draft copy, trashed backup) and swaps a copy's content back into its
* original. Meta is copied byte-exact: the stored meta_value strings are read and
@@ -14,6 +14,8 @@ use LIT\MCP\Integrations\WordPress\Schema\ContentSchema;
use WP_Error;
use WP_Post;
defined('ABSPATH') || exit;
/**
* lit/post-duplicate: creates a draft copy of an entry (spec §6.2). The original –
* ID, URL, menu items, content – is not touched. A failure after the insert deletes
@@ -16,6 +16,8 @@ use LIT\MCP\Integrations\WordPress\Schema\ContentSchema;
use WP_Error;
use WP_Post;
defined('ABSPATH') || exit;
/**
* lit/post-publish: publishes an entry immediately. A scheduled ("future") or
* future-dated entry is republished now instead of staying in the future (spec §6.2);
@@ -20,6 +20,8 @@ use Throwable;
use WP_Error;
use WP_Post;
defined('ABSPATH') || exit;
/**
* lit/post-replace-with-copy: swaps a copy made by lit/post-duplicate into its original
* (spec §6.2). ID, URL, status and menu items of the original stay. rev of the original
@@ -17,6 +17,8 @@ use LIT\MCP\Integrations\WordPress\Schema\ContentSchema;
use WP_Error;
use WP_Post;
defined('ABSPATH') || exit;
/**
* lit/post-schedule: schedules an entry for publication at a future point in time
* (site local time). An entry that is currently published stays live until then only
@@ -16,6 +16,8 @@ use LIT\MCP\Integrations\WordPress\Schema\ContentSchema;
use WP_Error;
use WP_Post;
defined('ABSPATH') || exit;
/**
* lit/post-trash: moves an entry to the trash – never a permanent delete. Trashing the
* static front page, the posts page or an entry linked in a menu is allowed but named in
@@ -15,6 +15,8 @@ use LIT\MCP\Integrations\WordPress\PostGuard;
use LIT\MCP\Integrations\WordPress\Schema\ContentSchema;
use WP_Error;
defined('ABSPATH') || exit;
/**
* lit/post-unpublish: withdraws a published, scheduled or private entry back to draft.
* An entry that is already not public (draft, pending, auto-draft) is a no-op.
@@ -7,6 +7,8 @@ namespace LIT\MCP\Integrations\WordPress\Status;
use RuntimeException;
use Throwable;
defined('ABSPATH') || exit;
/**
* Thrown by PostCloner::replace(). $parts lists the parts of the original replace()
* had started to write (post, meta, terms) – only those need to be restored.
@@ -9,6 +9,8 @@ use LIT\MCP\ChangeLog\ChangeLog;
use Throwable;
use WP_Post;
defined('ABSPATH') || exit;
/**
* Runs the one WordPress call of a status tool (wp_update_post(), wp_trash_post()). If a
* hook throws inside it, the row may already be written – the entry's state is unknown,
@@ -39,6 +39,8 @@ use LIT\MCP\Integrations\WordPress\Status\PostScheduleAbility;
use LIT\MCP\Integrations\WordPress\Status\PostTrashAbility;
use LIT\MCP\Integrations\WordPress\Status\PostUnpublishAbility;
defined('ABSPATH') || exit;
/**
* WordPress basics (spec §6): content, status, copies, revisions, menus, settings, and the
* rendered-content summary lit/page-render-summary (spec §5.7 – useful for every entry).