Issue #3615848: Send bundle and page path in entity_context so AI Context can scope unsaved entities and Site Sections.

Description

Closes Send bundle and page path in entity_context so ... (#3615848) • Unassigned

AiRequest::doRequest() attaches entity_context request metadata so subscribers such as AI Context can scope pushed context to the entity being edited. It only worked for saved entities: on an add form the id is empty, validateEntityContext() returned NULL, and nothing was sent. The bundle we already derive for saved entities is exactly what an unsaved entity needs, and hook_form_alter() already knows it. Separately, the request goes to /api/ai-ckeditor/request/{editor}/{plugin}, so subscribers cannot tell which page the editor is on.

Changes

  • ai_ckeditor_form_alter() adds bundle to the drupalSettings entry alongside entity_type and id.
  • AiDrupalDialog.js reads the bundle and sends entity_bundle plus page_path. getPagePath() strips drupalSettings.path.baseUrl. js/ai_ckeditor.js forwards both hidden fields.
  • AiCKEditorDialogForm carries entity_bundle and page_path as hidden fields and passes them to the plugin form settings.
  • AiRequest:
    • Injects entity_type.bundle.info.
    • validateEntityContext() gains an optional third $bundle argument. With an id present, behaviour is unchanged: load, check view access, use the loaded entity's bundle, ignore the client value. With an empty id, validateUnsavedEntityContext() accepts the bundle only if the type is a content entity, the bundle exists, and the current user has create access.
    • normalizePagePath() accepts a path with a single leading /, strips query, fragment, and $request->getBasePath(), and rejects relative, protocol-relative, and absolute URLs, NUL bytes, and values over 2048 bytes. The result is added to entity_context as path.
  • Docs: docs/developers/index.md documents the full entity_context shape (entity_type, bundle, id, path), including the content-entity unsaved rule and base-path strip.

API notes

  • AiRequest::__construct() gains a required EntityTypeBundleInfoInterface argument. create() is updated. Subclasses that override the constructor or validateEntityContext() must adjust their signatures.
  • validateEntityContext() is protected. The new argument is last with a default, so existing parent:: calls with two arguments keep working.
  • All entity_context values are untrusted hints for matching. They never load or grant access on their own; the saved-entity path still requires view access and the unsaved path requires create access.

Pairs with AI Context

AI Context 1.0.x-dev reads bundle and path from entity_context after #3586510+s. With both changes, a context item scoped with Entity Types = Content: Pirate post is pushed while writing a new Pirate post, and Site Sections items match the edited page instead of the API endpoint.

Testing instructions

Requires ai 1.5.0-alpha1 or later and ai_context 1.0.x-dev with #3586510+s.

  1. Create content type "Pirate post". Create a context item scoped with Entity Types = Content: Pirate post whose text says to talk like a pirate, and a second item scoped with Site Sections custom pattern /node/add/*. Publish both.

  2. Enable Generate with AI on a text format with a working chat provider. Enable push for the CKEditor completion consumer at /admin/config/ai/context/settings/consumers in Relevant mode. Turn on AI Context debug logging.

  3. Rebuild caches and hard-refresh the browser so the rebuilt js/build/aickeditor.js is loaded (npm install && npm run build on a dev checkout).

  4. Open /node/add/pirate_post and use Generate with AI in the body. Expected: the response is in pirate voice, and Recent log messages show both items selected.

  5. Save the node, edit it, repeat. Expected: the pirate item is selected, the /node/add/* item is not.

  6. Open an Article and repeat. Expected: neither item is selected.

  7. Run the tests:

    vendor/bin/phpunit web/modules/contrib/ai_ckeditor/tests/src/Unit/AiRequestPagePathTest.php
    vendor/bin/phpunit web/modules/contrib/ai_ckeditor/tests/src/Kernel/ValidateEntityContextTest.php
    vendor/bin/phpunit web/modules/contrib/ai_ckeditor/tests/src/Kernel/EntityContextMetadataTest.php
    vendor/bin/phpunit web/modules/contrib/ai_ckeditor/tests/src/Functional/AiRequestControllerTest.php
    vendor/bin/phpunit web/modules/contrib/ai_ckeditor/tests/src/FunctionalJavascript/EntityContextJavascriptTest.php

Verified locally against the real endpoint with Sonnet on Quant Cloud and ai_context on the #3586510+s branch: an unsaved pirate post with entity_bundle and a page_path carrying a query string selected both items and answered in pirate voice; a saved node sent with a wrong client bundle selected only the Entity Types item because the bundle was re-derived from the loaded node.

Tests added

  • Unit AiRequestPagePathTest: 16 accepted and rejected path hints, including subdirectory base-path strip and prefix collision.
  • Kernel ValidateEntityContextTest: unsaved known bundle; unknown or empty bundle; no create access; config entity type rejected; client bundle ignored for a saved entity.
  • Kernel EntityContextMetadataTest: unsaved payload with empty id and path reaches PreGenerateResponseEvent.
  • Functional AiRequestControllerTest: unsaved entity with bundle and page path is accepted; hostile path is isolated in testHostilePagePathDoesNotBreakRequest with create access.
  • FunctionalJavascript EntityContextJavascriptTest: getEntityContext() replicas include entityBundle on edit, add, and non-entity pages.

Unit and kernel tests were run locally. Functional and FunctionalJavascript tests rely on CI.

Checklist

  • I have updated the MR title to use format: Issue #1234: My issue title.
  • I have updated the MR description to include: Closes #1234
  • I have performed a self-review of my own code
  • I have added or updated tests, or explained in the description why this change is not covered by tests
  • I have updated documentation for any new or changed functionality
  • I have written testing instructions and verified them locally
  • I have noted any required post-merge steps (config imports, cache rebuilds, manual changes)
  • This MR contains no breaking API or hook changes, or they are explicitly documented in the description

Post-merge: cache rebuild only. js/build/ is gitignored and compiled in CI.

AI Compliance

Note

Check the one that best describes your usage, or leave all unchecked if AI was not significantly used.

  • AI Assisted Code
    Mainly written by a human; AI used for autocomplete or partial generation under full human supervision.

  • AI Generated Code
    Mainly generated by AI, reviewed and approved by a human before this MR was created.

  • Vibe Coded
    Generated by AI and only functionally reviewed before this MR was created.

Edited by Kristen Pol

Merge request reports

Loading