**Title:** IconBox children are misaligned horizontally and start at the wrong vertical position

**Summary**
When an `IconBox` contains an icon and one or more children (e.g., text labels), the children are not correctly positioned. They appear misaligned horizontally and the vertical start point is offset by the icon’s top padding, causing the first child to be lower than expected.

**Observed behavior**
- Horizontal centering is inconsistent: each child’s x‑offset depends on the icon’s drawn width (`w`), not on the widest child. As a result, children of different widths are not centred on a common axis and may appear shifted left or right.
- Vertical placement of the first child uses `bounding_box.y + padding_top + bounding_box.height + vertical_spacing` instead of `bounding_box.y + bounding_box.height + vertical_spacing`. This pushes children down by the icon’s top padding, creating extra unwanted space.

**Expected behavior**
1. Children should be horizontally centred together. The reference width for centring should be the maximum width among all children (not the icon’s drawn width). Each child is then centred within that maximum width, so all children start at the same horizontal position regardless of their individual width.
2. The first child should be placed exactly below the icon’s bounding box, at `bounding_box.y + bounding_box.height + vertical_spacing`, without including the icon’s internal padding.
3. The placement calculation should be extracted from the `draw` method into a separate, testable method. The existing TODO comment in `draw` (“Change X and initial Y, based on placement…”) already hints that this logic needs isolation and correction.

**Impact**
Any diagram element using `IconBox` (icons with labels underneath) currently shows wrong label alignment and spacing, making diagrams look unpolished and confusing.

**Relevant code area**
`gaphor/diagram/shapes.py` – the `IconBox` class, particularly its `draw` method and the child placement arithmetic.

Move placement for icon box children to separate method


The default property pages (`LineStylePage`, `NotePropertyPage`, and `NamePropertyPage` in `gaphor.diagram.propertypages`, plus `NamedElementPropertyPage` in `gaphor.UML.classes`) still use the `@transactional` decorator when mutating model state. The rest of the application has moved to passing an `event_manager` into property‑page constructors and using explicit `Transaction(self.event_manager)` contexts. This inconsistency means those pages may not participate correctly in the undo/redo system.

Update these property pages to:

- Accept an `event_manager` argument in their constructors.
- Remove the `@transactional` decorators and instead wrap model changes in `with Transaction(self.event_manager):` blocks.

All calls to these constructors (in the UI wiring, in the code that creates property pages, and in any related tests) must supply the `event_manager`. The behaviour of the pages should remain identical; only the transaction handling should change.

Use Transaction for default property pages


**Bug: Auto-layout misplaces nested elements and edges**

When I run the auto-layout feature (the "Layout diagram" action) on a UML/SysML model that contains elements nested inside other elements – for example, a class inside a package/component, or a part inside an internal structure – the layout output is incorrect. The nested items are placed far from their parent container’s visual bounds, often overlapping or appearing completely outside of the parent. Edges connecting them are also garbled: endpoint positions are wrong and the edge paths are offset.

From what I can tell, Graphviz uses a coordinate system with the origin at the bottom‑left, while Gaphor’s canvas uses top‑left. The conversion between these systems is currently applied too late or inconsistently for nested structures, causing coordinate offsets to be missing for children of subgraphs. Additionally, edges that were originally at the top level of the diagram may be automatically moved into a subgraph after layout, and then their handle positions are not adjusted for the parent container’s transformation matrix, causing them to appear displaced.

**Expected behavior:**
- Nested elements should be placed correctly within their parent container, using the parent’s local coordinate system and respecting the container’s offset.
- Edges’ handle positions should match the layout’s calculated coordinates, accounting for any parent container’s transform when the edge is part of that subgraph.
- The coordinate conversion from Graphviz’s system to Gaphor’s should be applied as soon as the layout output is read, so that subsequent positioning logic works entirely in Gaphor’s native coordinates.
- Moving an edge from the top‑level to a nested subgraph shall not introduce placement errors; the edge’s handles must be offset by the parent’s transform.


```
Title: UML parser uses plain strings instead of enumeration members for visibility and direction

When parsing UML text notation, the parser assigns `visibility` on features (for symbols `#`, `+`, `-`, `~`) as plain strings such as `"public"`, `"protected"` instead of using the corresponding members from `VisibilityKind` (defined in `gaphor/UML/uml.py`). Similarly, the default direction for operation parameters is set to the string `"in"` rather than `ParameterDirectionKind.in_`.

Observed problems:
- Model objects end up holding a `str` where the data model expects an enum member.
- Save/load round-trips may be unreliable or error-prone because the serialization layer might not handle the raw string the same way as a proper enum value.
- Mismatched types can lead to subtle bugs in checks or comparisons that assume enum membership.

Expected behavior:
- The visibility setter should store the appropriate `VisibilityKind` member (`VisibilityKind.public`, `VisibilityKind.protected`, `VisibilityKind.private`, `VisibilityKind.package`) instead of a plain string.
- The default parameter direction should be `ParameterDirectionKind.in_` (the enum member), not the string `"in"`.
- The fallback for unrecognised visibility symbols (which currently tries to delete the visibility attribute) should continue to work; it already uses `contextlib.suppress(AttributeError)` and does not depend on the value being a string.

Impact:
Any model parsed from text notation will have incorrectly typed enumeration attributes, potentially affecting downstream code that expects proper enum instances. This also risks incorrect model serialization.
```


Title: Drag-and-drop to top level in model browser tree is broken
Priority: high
Labels: bug, dnd, namespace-model

### Problem
Dragging an element in the tree navigator (the model browser) and dropping it at the top level — i.e., into the empty area at the top of the tree, making it a direct child of the root — no longer works.  Previously this would unparent the element and place it as a root-level item.  Now the drop is silently ignored; elements can still be dropped onto other elements (to become their children), but they cannot be returned to the top level via drag and drop.

### Expected behaviour
- Dropping an element onto the top-level tree area should move the element to the root of the model, i.e., set its owner to `None` (ungroup it).
- The drop should be allowed (drop indicator shown) and performed successfully.

### Implementation clues
The drag‑and‑drop logic is implemented in `NamespaceModel` (a `Gtk.TreeStore` subclass in `gaphor/ui/namespacemodel.py`).  The methods involved are `do_row_drop_possible` and `do_drag_data_received`, and the ownership change goes through `change_owner`.

A likely issue: when the drop target is a top‑level row (a path of depth 1), the code incorrectly treats that row as the destination element instead of extracting its parent (which would be the root, represented by `None`).  The path must be stepped up to obtain the parent element, similar to how `element_for_path` is used, but with the parent path.  For a top‑level drop, the parent path is empty and the element should be `None`.

Additionally, the helper `change_owner` currently does not handle `new_parent=None` — it should detect that situation and use `ungroup` to remove the element from its current owner.

### Acceptance criteria
- Dragging an element from anywhere in the tree and dropping onto the top level unparents it and makes it a root element.
- Existing drag‑and‑drop behaviour (dropping onto another element as a child) continues to work.
- No regressions in other tree interactions.


**Request:** Implement a simple `:has()` pseudo-class operator for the styling selector language.

- A selector `«name»:has(«selector»)` must match an element if the element's local name equals `<name>` AND the element contains at least one descendant (at any depth, not only immediate children) that matches the nested `<selector>`.
- The nested selector can itself be a simple name (e.g., `nested`) or any other valid selector (though the initial implementation only needs to handle simple name selectors correctly).
- If an unrecognized pseudo-class name is used, raise a `SelectorError` (similar to how unknown selectors are handled).
- The existing combinators (` `, `>`) and simple name selectors (`classitem`, `nested`, etc.) must continue to work as before.

Don't alter the public API beyond adding support for `:has(…)`.


Make the tree model create child branches lazily instead of eagerly. Currently, when a model element gains children, the tree model immediately builds a child list store for that element, even if the user never expands the node. This wastes memory and time for deep models. Instead, the child list store should only be created when the widget requests it (i.e., in the child_model callback). However, the tree view must still show an expander arrow as soon as a node acquires children, without forcing the child store to be materialized. So when a child element is added and the parent does not yet have a child store, the parent’s row must be invalidated so the widget knows it now has children; the actual store will be built on demand when the user expands the node. The existing contract for adding, removing, and re-parenting elements must remain correct, including removal of a child that was never expanded (i.e., where no child store exists for the parent). The not-yet-created branch should still be representable in the tree item lookup (e.g., via tree_item_for_element) so that the parent’s row can be notified.


Bug: unnecessary `pass` statement in generated Python classes

When generating Python source code from UML classes (using the codegen module), every generated class body unconditionally includes a `pass` statement. This happens even when the class has real attribute definitions, resulting in output like:

```python
class Foo(Base):
    name: attribute[str]
    pass
```

The extra `pass` is messy and unintended.

Two issues feed into this:
1. The attribute-iteration logic yields the literal string `"pass"` for derived attributes, which should instead produce no output at all (they should be silently skipped).
2. When a class has no generated fields at all, a trailing `pass` is needed to keep the class body syntactically valid, but that fallback should be decided at the point where the full class body is assembled and printed — not inside the attribute iterator.

Desired behavior:
- A generated class body contains `pass` **only** when no non-derived field definitions are produced by the iteration logic.
- Derived attributes should not contribute any line to the generated class body, neither a field definition nor `pass`.
- If a class has real attributes, no `pass` should appear.

This affects the code in `gaphor/codegen/coder.py`, specifically the way the `Coder` iterator yields attribute lines and how the top-level `coder()` function prints the class body. The fix should move the responsibility for emitting a fallback `pass` from the iterator to the consumer that prints the class, and it should stop considering derived attributes as a reason to emit anything.

Only "pass" a class when no fields are printed


Bug: `is_profile_class` and `is_in_toplevel_package` in `gaphor/codegen/coder.py` are stubs that always return `None`/implicitly falsy, so client code that uses them to filter or classify UML Classes silently fails. This means, for example, that code generation cannot correctly handle classes defined inside a Profile or inside a specific top-level package.

The stubs need real implementations covering the following semantics:

- **Profile membership:** Given a `UML.Class`, determine if it lives anywhere inside a `UML.Profile`. That is, walk up the `owningPackage` chain from the class’s immediate owner; if any package encountered is an instance of `UML.Profile`, return `True`. (The existing placeholder is named `is_profile_class`, but the intended check is really “is the class inside a Profile”, not “is the class itself a Profile class”. The name should be adjusted to reflect that – e.g., `is_in_profile` – so callers aren’t misled.)

- **Top-level package membership:** Given a `UML.Class` and a `package_name: str`, decide whether the class is owned (directly or transitively) by a top-level package with that exact name. A “top-level” package is one that has no `owningPackage`. Walk up the chain: if the topmost package (where `owningPackage` is `None`) has `name == package_name`, return `True`; otherwise `False`. The current stub `is_in_toplevel_package` must be implemented to perform this recursive check.

Both checks should handle the case where the class has no owning package (return `False`) and should not crash on any intermediate `None` values. The implementations should be added to `gaphor/codegen/coder.py` and follow the existing pattern of other helper predicates in that module.

Add functions to check for package rules


**Title:** `node-model` materialization cache loses resolved fields across queries, breaking sort on nested custom-resolver fields after a no-match filter query

**Background**
When using the `LocalNodeModel` to run consecutive queries on the same node type, the per-node `__gatsby_resolved` cache should accumulate resolved field values across multiple materialisation passes. When a query only requests a subset of fields (e.g. only those needed for filtering), a later query that needs additional fields (e.g. for sorting) must still be able to resolve and merge them correctly.

**Bug description**
After executing a `runQuery` with a filter that yields **no matching nodes** (for example, a `regex` filter that matches nothing), a subsequent `runQuery` on the same type that **does** match nodes and sorts by a nested field with a custom resolver (e.g. `Meta.Date`) returns incorrectly sorted results or fails to resolve the field values. The sorting appears to work on stale or missing data.

**Steps to reproduce**
1. Define a node type (e.g. `Test5`) that has a nested object field `Meta` whose sub-fields (`Date`, `Category`) are provided via custom field resolvers.
2. Execute `runQuery` on `Test5` with a filter like `{ id: { regex: "/non-existing/" } }` (returns no nodes).
3. Execute `runQuery` on `Test5` with a matching filter (`{ id: { regex: "/id/" } }`) and a sort specification: `sort: { fields: ["Meta.Date"], order: ["desc"] }`.
4. **Observed:** The returned nodes are not sorted by `Meta.Date` descending as expected.
5. **Expected:** The nodes are sorted correctly, using the custom-resolved `Meta.Date` values.

**Root cause analysis**
In `_doResolvePrepareNodesQueue`, resolved fields are currently merged into a *new* object computed from `_.merge(node.__gatsby_resolved || {}, resolvedFields)`. The resulting object is returned but **not assigned back** to `node.__gatsby_resolved`. Because the same node instance can be visited in multiple resolve passes (e.g. first for a filter-only pass, then again for a sort pass that needs additional fields), later passes may operate on a `node.__gatsby_resolved` that does not contain fields resolved earlier, causing loss of data and incorrect sorting/filtering behaviour.

**Desired change**
Ensure that `node.__gatsby_resolved` is always initialised (if missing) and then **mutated in place** with the newly resolved fields via `_.merge`, so that all resolved values are persistently accumulated on the node across materialisation batches. After this change, the sequence described above must produce correctly sorted results.

**Acceptance criteria**
- A `runQuery` with a non-matching filter followed by a `runQuery` with sorting on a custom-resolved nested field returns nodes sorted according to that field.
- The same node’s `__gatsby_resolved` survives across multiple `prepareNodes`/resolve passes and never loses previously resolved sub-object fields.
- The concrete scenario reported in #28047 (regex filter then sort on `Meta.Date`) works reliably.

fix(gatsby): fix materialization edge case (#28244)


# `downloadContentfulAssets` should not fetch assets via `getNodesByType`

The asset-download step in `gatsby-source-contentful` currently discovers which files to download by calling `getNodesByType('ContentfulAsset')`.
That is unreliable in several real-world scenarios:

- **Warm / incremental builds** – `getNodesByType` returns *all* `ContentfulAsset` nodes that exist in the store, including nodes from previous runs that are not part of the current sync payload. This can trigger unnecessary downloads and squander network / cache capacity.
- **Uncommitted nodes** – depending on internal timing the freshly created asset nodes may not yet be visible to `getNodesByType`, causing the download step to **skip** assets that need to be fetched.
- **Multiple spaces** – if multiple Contentful sources are configured the store may contain asset nodes from a different space, which should never be passed to the download logic for the current space.

## Required behaviour

- Only the asset nodes that were **created (or updated) during the current `sourceNodes` execution** should be handed to the download step.
- The list should come directly from the asset processing code, *not* from the store.

## Concrete ask

1. **Collect the concrete asset nodes** that `createAssetNodes` produces. Today the function returns promises that resolve to nothing useful; it must resolve to the full node objects (so that fields like `file`, `node_locale`, `contentful_id` etc. are available for the download logic).
2. **Accumulate those node objects** in a plain array inside `sourceNodes` while processing the sync assets.
3. **Pass that array** explicitly into `downloadContentfulAssets` instead of letting the function call `getNodesByType`. The function signature should accept an `assetNodes` parameter and iterate over it directly.
4. Keep the existing download semantics unchanged (unique cache key per `contentful_id` + `node_locale`, worker distribution, reporting progress).

This makes the data flow deterministic and removes the dependency on the global store state during asset downloads.

fix(gatsby-source-contentful): don't rely on getNodesByType (#34276)


The `gatsbyRemarkPlugins` option for `gatsby-plugin-mdx` allows plugins to modify the MDX abstract syntax tree before compilation. Some of those plugins work by inserting new `import` nodes into the tree (for example, to inject a default import for a helper library). However, when the `findImports` utility (used to determine which imports are needed in the component scope) scans the AST, it currently does so before the remark plugins have been applied. As a result, any imports injected by plugins are completely ignored.

The observable effect is that the generated MDX component is missing those imports, which leads to ReferenceErrors at runtime when the code tries to use the injected symbols. The expected behavior is that all import statements—whether present in the original source or added by remark plugins—are collected and included in the scope imports of the MDX node.

To fix this, the pipeline that parses the MDX source must first apply the configured remark plugins (including `gatsbyRemarkPlugins`) before extracting import declarations. This applies to the `findImports` (or `findImportsExports`) step inside `packages/gatsby-plugin-mdx/utils/gen-mdx.js`.

fix: gatsby-plugin-mdx's findImports not running plugins (#26191)


**Title:** End reporter activities when errors are thrown during sourceNodes

**Context**
In `gatsby-source-drupal`, `sourceNodes` starts several `reporter.activityTimer` timers (e.g., “Fetch data from Drupal”, “Remote file download”, “loading Drupal content changes”). If an error occurs while any of these are active, the timer is never ended. This leaves dangling progress indicators in the CLI, can mask build failures, and may prevent proper error reporting on more recent Gatsby versions that support `activity.panicOnBuild`.

**Expected behavior**
When an error is thrown inside the data-fetching, file-download, or webhook-update block, the corresponding activity **must** be ended before the error propagates. The plugin should additionally use `activity.panicOnBuild` if it exists (newer Gatsby), or fall back to throwing normally after ending the activity (older Gatsby). This ensures the build fails consistently and the activity is cleaned up regardless of the error path.

**Affected areas**
- The `axios.get` fetch loop that retrieves Drupal entity data (around the `drupalFetchActivity` timer).
- The `asyncPool` block that downloads remote files (around `downloadingFilesActivity`).
- The `webhookBody` handler that processes update webhooks (around `changesActivity`).

**Given**
- The `sourceNodes` function exports from `packages/gatsby-source-drupal/src/gatsby-node.js`.
- `reporter.activityTimer` returns an object with `start()` and `end()` methods; newer Gatsby versions add `panicOnBuild(error)`.
- `nodeFromData`, `downloadFile`, `handleWebhookUpdate`, etc. may throw synchronously or asynchronously.

**Then**
After implementing, every path that starts one of these timers must guarantee `activity.end()` is called exactly once, even when the operation fails, and the error that caused the failure must still cause the build to stop (directly or via `panicOnBuild`). No functional change should be introduced for the normal, error‑free case, and the plugin must remain compatible with both older and newer versions of Gatsby.

fix(gatsby-source-drupal): end activities if errors are thrown (#25447)


**Title:** Third-party schemas with recursive Query references fail during schema rebuild

**Background**
Gatsby supports integrating third-party GraphQL schemas (e.g., via `gatsby-source-graphql`). Some of these schemas—especially Relay Classic implementations like the GitHub API—introduce types that contain fields whose type is the schema’s own `Query` type (e.g., `type GitHub { relay: Query }`). When Gatsby composes the final schema, it must replace the third-party `Query` type with its own root `Query` type.

**Problem**
During `buildSchema` and subsequent rebuilds, the type composition process eagerly creates type composers for third-party types. If a third-party type has a field whose type is the third-party `Query` type (or a chain that resolves to it), the eager creation causes a duplicate `Query` type composer to be registered. This produces an invalid schema because Gatsby’s `Query` and the third-party `Query` collide, and the schema-customization phase either throws an error or silently breaks the schema.

The core issue is that certain third-party types define their field map as a plain object (non‑thunk) containing a reference to the third-party `Query` type. Gatsby then processes these fields before the `Query` type is replaced, leading to conflicts.

**Desired behavior**
- Gatsby must not fail when a third-party schema contains a type with a direct or indirect reference back to its own `Query` type.
- The resulting composed schema must be valid and queryable.
- The fix must be backward-compatible: schemas that do **not** use recursive Query references must continue to work as before.

**Constraints / Notes**
- The `addThirdPartySchemas` pipeline is the integration point; any fix should be localized there.
- The adjustment should only affect types whose field map is a concrete object (not already a thunk).
- When rewriting field types that reference the third-party `Query` type, the rewrite must use Gatsby’s `Query` type name (as already done for non-recursive third-party fields).
- The existing `createResolvers` overrides that touch third-party types must still be applied correctly after this change.

**Acceptance criteria**
1. Adding a third-party schema whose `Query` type appears as a field type in one of its own output types (including indirectly via an intermediate type) does not cause a schema-build error.
2. The built schema passes `assertValidSchema`.
3. All existing third-party schema integrations (e.g., GitHub, CMS schemas without recursion) continue to build and rebuild without regressions.

fix(gatsby): do not fail on 3rd-party schemas with relay-classic support (#30318)


**Title:** Intermittent wrong sort order when sorting on a field backed by a custom resolver (materialized field)

**Background:**
In Gatsby’s data layer, some fields are “materialized” when they use a `resolve` function and are needed for sorting or filtering. The resolved values are cached in `node.__gatsby_resolved` and also persisted in the Redux store’s `resolvedNodesCache`. However, nodes are now held weakly and can be garbage‑collected mid‑query, causing `__gatsby_resolved` to be lost even though the cache still holds valid values.

**Observed behavior:**
When executing a query that sorts on a materialized field, the result order is sometimes incorrect (non‑deterministic). Investigation suggests that after a GC, the resolved value for the field becomes `undefined` for some nodes, causing them to sort incorrectly.

**Root cause:**
- In `getGatsbyNodePartial` (used during filter/index preparation), if a requested dotted field starts with `__gatsby_resolved.` but the node object lacks the `__gatsby_resolved` property, the lookup falls back to `undefined` and never consults the persisted `resolvedNodesCache`.
- During materialization (`_doResolvePrepareNodesQueue`), the newly computed `resolvedFields` are saved directly, overwriting any previously cached resolved values. If materialization for the same type happens more than once (e.g., for different sets of resolved fields), earlier resolved fields can be dropped.

**Expected behavior:**
Sorting on materialized fields must return a stable, correct order regardless of garbage collection. Resolved fields must remain consistent across multiple materialization passes.

**Request:**
1. When a field path starts with `__gatsby_resolved.` and the node’s `__gatsby_resolved` is missing, retrieve the resolved sub-object from the Redux store (`resolvedNodesCache`) for the given node type and ID, attach it to the node object, and then proceed with the lookup.
2. When materializing fields for a type, merge the newly resolved fields with any previously cached resolved fields for the same node ID (from `resolvedNodesCache`), so that earlier resolved fields are preserved. The merged result must be saved back to the cache and applied to `node.__gatsby_resolved`.

**Affected areas:**
- `packages/gatsby/src/datastore/in-memory/indexing.ts` – `getGatsbyNodePartial`
- `packages/gatsby/src/schema/node-model.js` – materialization logic (`_doResolvePrepareNodesQueue`)

No API changes are needed; this is purely a robustness fix for the internal caching and materialization pipeline.

fix(gatsby): fix intermittent wrong sort results when sorting on materialized field (#35271)


**Title:** Interface queries fail to resolve fields that require custom resolution on concrete types (e.g., `@proxy`)

### Summary
When querying a GraphQL interface type that has a field implemented with a custom resolver on its concrete type (such as `@proxy`), the field is not materialized. This causes missing values, incorrect `null`s, and filtering failures.

### Observed behavior
- A schema defines an interface with `@nodeInterface`, e.g.:
  ```graphql
  interface FooInterface @nodeInterface {
    id: ID!
    slug: String
  }
  ```
- A concrete type implements the interface and overrides the `slug` field using a custom extension, e.g.:
  ```graphql
  type FooConcrete implements Node & FooInterface {
    id: ID!
    slug: String @proxy(from: "slugInternal")
  }
  ```
- When querying `allFooConcrete`, the `slug` field is correctly resolved.
- When querying `allFooInterface` (the interface connection), the `slug` field returns `null`, and any filter that uses `slug` (e.g., `filter: { slug: { eq: "some-value" } })` returns no results, even though the concrete node has the expected value.

### Expected behavior
A field defined on a concrete type with a custom resolver (like `@proxy`) should be resolved when the query is made through the interface type. Queries on `allFooInterface` must behave identically to queries on `allFooConcrete` for that field, including correct filtering and returned values.

### Impact
This makes it impossible to build reliable abstract queries over polymorphic node sets when any concrete type uses field-level custom resolvers. Workarounds (e.g., querying each concrete type separately) break the GraphQL abstraction and complicate client code. The same misbehavior likely affects other resolver-driven field extensions and possibly `link` fields on interfaces that rely on type-specific resolution.

### Additional context
The problem stems from the internal logic that determines which fields need “resolving” before they can be returned. That logic currently only inspects the field on the abstract type’s composer and does not check the concrete type’s composer for the `needsResolve` extension. As a result, materialization is skipped for fields that the interface itself does not declare as needing resolution, even though the concrete implementation does require it.

fix(gatsby): Fix not materializing concrete fields (#17942)


## Bug: `findPageByPath` picks an arbitrary matching client‑only route instead of the most specific one

When multiple pages define overlapping `matchPath` patterns (for example, a wildcard catch‑all and a parameterized route), Gatsby’s `findPageByPath` helper may return a less‑specific page. This causes SSR and client‑side navigations to render the wrong template, even when a more precise match exists.

### Steps to reproduce

1. Create several pages that share a common prefix, each using `matchPath`:
   - A splat/catch‑all: `matchPath: "/app/*"`
   - A two‑segment parameterized route: `matchPath: "/app/:p1/:p2"`
   - A mixed route: `matchPath: "/app/:p1/page"`
2. Also include a static page that overlaps (e.g. `/app/static/page/`).
3. Request a path like `/app/foo/bar` or `/app/foo/page`.

### Expected behavior

- `/app/foo/bar` should resolve to the page with `matchPath: "/app/:p1/:p2"` because it has two specific dynamic segments, which is more specific than `/*`.
- `/app/foo/page` should resolve to `matchPath: "/app/:p1/page"`, not the splat.
- `/app/static/page` should resolve to the exact static page (`/app/static/page/`), and should *not* match any `matchPath`.
- The selection must be **deterministic** and must **not depend on the order** in which pages happen to be stored internally.

### Actual behavior

`findPageByPath` iterates over pages and returns the *first* page whose `matchPath` matches the requested path, regardless of specificity. This means the result depends on the insertion order of pages in the state, often causing the wildcard `/*` route to “win” when it appears earlier, even though a more specific route exists.

The same function also fails to give static pages priority over client‑only `matchPath` routes when both would match.

### Impact

- SSR pages that rely on correct parameter extraction (e.g. `getServerData`) receive wrong `params` and render incorrect content.
- Client‑only routes behave unpredictably on hard‑navigation and `<Link>` clicks, breaking user‑facing flows.
- The problem cannot be reliably worked around by ordering page files or `createPage` calls, because the order is implementation‑defined and not guaranteed.

### Acceptance criteria (for a fix)

- Introduce a specificity‑based ranking: **static segment > dynamic (`:param`) segment > splat (`*`) segment > root**.
- When a static page (exact path) exists, it should always beat any `matchPath`.
- The behavior must be the same regardless of the order of pages in the `IGatsbyState.pages` map.
- The solution must not break existing exact‑path lookups or the 404 fallback.
- Existing functions `findPageByPath` (in `packages/gatsby/src/utils/find-page-by-path.ts`) should be updated – no external API changes required.

fix(gatsby): Wrong route resolved by findPageByPath function (#34070)


We need the server-side rendered output of `gatsby-plugin-image`’s `StaticImage` (and any `GatsbyImage` that uses the same data) to always include a correct `sizes` attribute on the main `<img>` element. Currently, `sizes` is either missing or incorrect in SSR for fixed, fluid, and constrained layouts.

**Expected behaviour:**

- **Fluid layout:** `sizes` must be `100vw`.
- **Fixed layout:** `sizes` must equal the image’s display width (e.g. `500px`), matching the fixed slot.
- **Constrained layout:** Default `sizes` must be `(min-width: ${maxWidth}px) ${maxWidth}px, 100vw`. When a `sizes` prop is passed (e.g. `sizes="100vw"`), it must replace that default entirely – no concatenation or merging.
- The attribute must appear on the `<img data-main-image>` (or equivalent) tag in SSR, for all layouts.
- The `sizes` prop on `StaticImage` must be honoured as a full override, and the default must be calculated internally from `maxWidth` / `width` / layout.

**Out of scope (no changes needed):**
- Client-side rendering behaviour, apart from ensuring consistency with the SSR output.
- Changes to how `srcSet` is computed.

This is a bug-fix: the fields exist in the data but SSR is not wiring them to the DOM correctly, or the default value is not being supplied for constrained/fluid cases.

fix(gatsby-plugin-image): Fix handling of sizes prop in SSR (#28835)


**Title:** `gatsby-core-utils`: `fetchRemoteFile` ignores existing cache, re-downloads unnecessarily, and mishandles copy to public output directory

**Background**
The `fetchRemoteFile` utility is supposed to cache remote assets based on a URL + cache key. Once a file is downloaded, subsequent identical requests should return the already-cached file path without re-fetching. When the requested output directory lies inside the Gatsby public folder, a copy of the cached file should be placed there; otherwise, the original cached path can be returned directly.

**Observed behavior**
- Re-downloads happen even when a valid cache entry exists on disk.
- The logic that decides whether to copy to the public directory appears inverted: it checks the cached file’s directory instead of the intended output directory.
- The deduplication set (`alreadyCopiedFiles`) is used backwards, causing copies to either be skipped when they should happen, or attempted multiple times.
- The path stored in the in-flight metadata does not correctly strip the directory portion (may include a leading slash or fail to strip when slashes differ), potentially causing cache misses.

**Expected invariants**
1. **No redundant downloads** – If a file for the same URL and cache key has already been fetched and persisted to the cache directory, subsequent calls must return the existing path without issuing a new HTTP request.
2. **Public‑directory detection** – A copy should be created only when `options.directory` (the requested output directory) resides under the Gatsby public folder (i.e., `{root}/public`). The decision must be based on the *output* directory, not the cache directory.
3. **Single copy per destination** – When a copy is needed, it must happen exactly once per unique output path, even under concurrent requests. The `alreadyCopiedFiles` set logic must be corrected so that the check and insertion are race‑free (already guarded by a mutex).
4. **Accurate cache‑key metadata** – The `path` stored in the per‑URL/build‑ID metadata must be derived by removing the slash‑normalized directory prefix, including its trailing separator, so that later lookups match consistently.

**Scope**
Adjust the caching and copy branches inside `fetchRemoteFile` (and the metadata setter in `fetchFile`) to satisfy the invariants above. Edge cases: caching across builds (same `cacheKey` but different build IDs), and concurrent fetches for the same URL should continue to work thread‑safely.

fix(gatsby-core-utils): fix caching when using remote-file (#34951)


Implement deduplication and callback aggregation for `<Script>` in `gatsby-script` so that when multiple instances share the same `id` or `src`, they collectively invoke all `onLoad`/`onError` callbacks from every instance after the script loads or fails. The first-rendered instance is allowed to omit callbacks; later instances that supply callbacks must still receive them. Only deduplicate when an `id` or `src` is present—scripts without either should each inject independently. Ensure that unmounting the component that originally injected the script correctly removes the aggregated event listener rather than the unwrapped callback.

feat(gatsby-script): Duplicate script callbacks if no injected script callbacks (#35717)


In `gatsby-source-wordpress`, when `schema.perPage` is small (or many distinct media items are referenced on a single node), `fetchMediaItemsBySourceUrl` only processes the first page of results. The promise resolves immediately with that first batch, discarding subsequent pages. As a result, many referenced media item URLs never get fetched, and their file links are not rewritten to local file nodes.

Make `fetchMediaItemsBySourceUrl` collect all pages of results before resolving, so that every referenced media item URL is fully processed regardless of how many pages are needed.

fix(wordpress): ensure all file links are rewritten (#31652)


**Title:** `group` resolver breaks when field values collide with `Object.prototype` properties (e.g. `"constructor"`)

**Context:**
In `packages/gatsby/src/schema/resolvers.js`, the `group` resolver accumulates nodes using a plain object (`{}`) as the map of group keys (field values) → nodes. This causes incorrect behaviour when a field value matches a built-in property name on `Object.prototype`, such as `"constructor"`, `"toString"`, or `"valueOf"`. The accumulator inherits the prototype entry rather than creating a new group, leading to missing or corrupted group results.

**Steps to reproduce:**
1. Have a content type with a field that stores arbitrary strings (e.g., `frontmatter.tags`).
2. Insert a node where that field equals a reserved keyword, e.g. `"constructor"`.
3. Run a query like:
   ```graphql
   {
     allMarkdown {
       group(field: frontmatter___tags) {
         field
         fieldValue
       }
     }
   }
   ```
4. The group for `"constructor"` is absent or merged into the prototype property.

**Expected behaviour:**
Grouping must treat field values as opaque keys and must not be affected by the prototype chain of the accumulator object. Any string value—including those that collide with `Object.prototype` names—should produce a distinct group with the correct `fieldValue`.

**Suggested approach:**
In the `group` resolver, replace the plain accumulator object with one that does not inherit from `Object.prototype` (e.g., created via `Object.create(null)`). This ensures that all field values, including reserved words, are handled purely as own properties. No other changes to the grouping logic are required.

**Acceptance criteria:**
- Grouping by a field whose value is `"constructor"` (or any other `Object.prototype` property name) yields a group with the expected `fieldValue`.
- Existing grouping behaviour for all other field values remains unchanged.
- No regressions in the `distinct` resolver or other helpers that may share similar patterns.

**Related:** issue #22603

fix(gatsby): Support grouping by reserved keywords (#22603)


### Feature Request: Limit Node Manifest Files Written to Disk

**Problem**
When a site has a very large number of nodes that trigger node manifest creation (e.g., thousands of content entries), Gatsby writes a correspondingly large number of node manifest JSON files to the `.cache/node-manifests` directory. This can degrade build/develop performance and consume excessive disk space. There is currently no upper bound on how many manifest files are created.

**Desired Behavior**
Gatsby should enforce a configurable upper limit on the number of node manifest files written to disk per build/develop session.

**Acceptance Criteria**

1. **Default cap**: By default, at most **10,000** node manifest files are persisted. If fewer than 10,000 manifests exist, all are written—no truncation occurs.

2. **Configurable cap via environment variable**: Setting the environment variable `NODE_MANIFEST_FILE_LIMIT` to a positive integer overrides the default. For example, `NODE_MANIFEST_FILE_LIMIT=500` means at most 500 manifests are written.

3. **Prioritization when the limit is exceeded**: When the number of pending node manifests exceeds the limit, Gatsby must prioritize which manifests to keep. The priority rules are:
   - Manifests that have an `updatedAtUTC` timestamp are **preferred** over those without one.
   - Among manifests with `updatedAtUTC`, those with **more recent** timestamps are preferred (newest first).
   - Manifests without `updatedAtUTC` are kept only if room remains after all timestamped manifests are included.

4. **Idempotency and determinism**: Given the same set of input manifests and the same limit, the set of manifests written to disk should be deterministic. Tie-breaking for manifests sharing the same `updatedAtUTC` need not be specified but should not cause non-deterministic failures.

5. **No functional impact below the limit**: Sites with fewer manifests than the cap should see no change in behavior—all manifests are written as before.

6. **Graceful handling of invalid or zero limits**: If the environment variable is set to a non-numeric value, the default of 10,000 is used. A limit of 0 should write zero manifests. Negative limits should be treated as invalid and fall back to the default.

**Out of Scope**
- Any change to *when* node manifests are created or invalidated.
- Changes to the on-disk manifest format or the node manifest API surface (`unstable_createNodeManifest`).

fix(gatsby): limit node manifest creation limit (#35359)


**Title:** Stale component data dependencies cause redundant query re‑runs after successful dirty‑query execution

**Description**

Incremental builds (including `gatsby develop` hot‑reloads and cache‑persisted `gatsby build` runs) re‑execute page and static queries on every cycle even when the underlying data hasn’t changed.

**Steps to reproduce**

1. Create a Gatsby site where a page query fetches a node that contains a linked node (e.g., a `DirtyTest` node with a `link___NODE`).
2. Start `gatsby develop` (or perform a build with an existing cache from a previous run).
3. Modify the **linked** node’s data so that the page query becomes dirty and re‑runs.
4. Without any further data changes, either:
   - trigger a hot‑reload (e.g., touch an unrelated file), or
   - stop and restart the develop server, or
   - run a subsequent `gatsby build` with the same cache.

**Expected behaviour**

- The query runs only in step 3 because its data dependency became dirty.
- In step 4 the query should **not** run again; the previously recomputed result should be reused from cache.

**Actual behaviour**

The same query is re‑executed on every incremental cycle, even though no data that the query depends on has changed since it last ran.

**Why this happens**

When Gatsby detects that a node has changed, it marks the affected queries as “dirty”. The query‑runner then re‑executes those queries, but the internal mapping that tracks which components (pages / static queries) depend on which nodes is **not cleared** for the re‑run results. The stale dependency records cause the same queries to be considered dirty again during the next evaluation, leading to an endless chain of unnecessary re‑runs.

**Impact**

Unnecessary query execution significantly slows down incremental rebuilds and the developer feedback loop. The extra runs also consume resources without benefit, especially noticeable in sites with many interconnected nodes or large schemas.

**Request**

After re‑running queries that were marked dirty, clear the component‑level data‑dependency records for those query IDs so that subsequent cycles correctly recognise that the queries are up‑to‑date (no longer dirty) unless the data actually changes again. This cleanup should apply both to static queries and page queries.

fix(data-dependency-tracking): clear data dependencies when rerunning dirty queries (#24378)


When building a Gatsby site on Windows, some pages fail to include the static query data they depend on, while other pages may receive incorrect or duplicate static query assignments. Global static queries (e.g., those used in `gatsby-browser`) sometimes are not injected into every page.

**Steps to reproduce**
1. On a Windows machine, create a Gatsby project with multiple page templates and several `useStaticQuery` calls in shared components.
2. Run `gatsby build` (or the development server) and inspect the generated static‑query mappings.
3. Notice that certain templates are missing expected static query hashes, or that a template repeatedly appears with different hash arrays because its path is stored inconsistently.

**Expected behavior**
The mapping from page‑template file paths to static query hashes should be deterministic and complete: every template that transitively imports a component with a static query should list that query’s hash. On Windows, the mapping must work the same as on macOS/Linux, without any entries being dropped or duplicated because of path‑separator differences.

**Technical notes**
- The function `mapTemplatesToStaticQueryHashes` receives a compilation’s module graph (which uses platform‑native path separators) alongside state where static‑query component paths are already normalised to POSIX slashes.
- Because comparisons between webpack module resources and those pre‑normalised paths use strict equality, lookups silently fail when the separator conventions don’t match.
- Keys in the returned map (template paths) and values inside the dependency sets should also use a consistent slash style so that later consumers (e.g., when attaching static queries to pages) don’t miscount or deduplicate incorrectly.
- Special attention is needed for global static queries coming from `gatsby-browser`: their hashes must be added to every template, and the path used to register them must not differ from the template keys used elsewhere.

**Impact**
This bug prevents pages from hydrating the correct static query results on Windows builds, leading to missing data, blank sections, or build warnings about unused queries. It also creates non‑deterministic behaviour when the same project is built on different operating systems.

chore: add tests to static-query filepaths (#25978)


When a Gatsby plugin accesses the `cache` parameter inside the `onPreInit` API, the resulting error is cryptic and unhelpful.

**Observed behavior**
A plugin that calls `cache.get(...)` or `cache.set(...)` in its `onPreInit` implementation causes a low‑level runtime failure (e.g., `cache.get is not a function` or a deep internal stack trace from Gatsby). There is no indication that the real problem is using the cache before it is ready.

**Expected behavior**
Gatsby should detect this misuse and immediately terminate with a clear, descriptive error. The error should state that the `cache` instance is not available in `onPreInit` because the cache is initialised later in the bootstrap sequence. It should also identify the offending plugin by name, so the author can quickly fix the API placement.

**Impact**
Plugin developers are left debugging confusing stack traces without realising the root cause is a lifecycle restriction. A well‑formed fatal error (e.g., via `reporter.panicOnBuild`) would save significant troubleshooting effort and prevent silent failures.

**Additional context**
The `onPreInit` API runs very early, before Gatsby’s cache layer is set up. Plugins that need cache access should move their logic to a later API (such as `onPreBootstrap`). If the cache cannot be made available earlier, the runtime should at minimum reject any attempt to use it with a meaningful message.

fix(gatsby): do not initialize cache in onPreInit API (#26810)


**Feature Request: Programmatic IPC control of `gatsby develop`**

### Motivation
When `gatsby develop` is spawned as a child process with an IPC channel (e.g., via `child_process.spawn` with `stdio: ['ignore', 'ignore', 'ignore', 'ipc']`), there is no way for the parent process to send commands that are forwarded to the internal development server. This prevents graceful shutdown and other programmatic orchestration.

### Acceptance Criteria

1. **IPC message forwarding**
   When `gatsby develop` is running and has established an IPC channel with its parent, any message received on that channel must be forwarded to the internal development server process (the one managed by the `ControllableScript` instance).

2. **Graceful exit via IPC command**
   Sending a message of the form:
   ```json
   { "type": "COMMAND", "action": { "type": "EXIT", "payload": "SIGINT" } }
   ```
   from the parent must cause the development server to shut down cleanly. The `gatsby develop` process should exit with a normal termination code.

3. **Guards for message send lifecycle**
   If a message is sent to the internal process before that process has been started, the `ControllableScript.send()` method must throw a descriptive error (`"Trying to send a message before process started"`). This ensures callers are aware of the timing requirement.

4. **Correct error wording for other lifecycle guards**
   The error messages in `ControllableScript.onMessage()` and `onExit()` must read `"before process started"` instead of `"before process starter"` to be grammatically correct and unambiguous.

5. **Non-interference with existing IPC handlers**
   The forwarding mechanism must not alter or suppress any existing `message` listeners attached directly to the internal process (e.g., those used internally by `ControllableScript.onMessage()`). It should act as an additional, transparent relay.

6. **Process stability**
   The main `gatsby develop` process must remain stable and not crash when receiving arbitrary or unexpected messages over IPC. Message handling must be resilient to messages arriving at any point after the internal process has started.

### Additional Context
This capability is essential for tooling that wraps `gatsby develop` and needs to signal it to stop (e.g., integration test harnesses, CI monitors, or interactive CLI managers). The IPC channel is the natural communication path when spawning a child process with Node.js, and `gatsby develop` should support it natively.

fix(gatsby): enable ipc messaging on develop (#26221)


`fetchMediaItemsBySourceUrl` and `fetchMediaItemsById` in the WordPress source plugin only process the first page of media items when the item count exceeds `schema.perPage`. Subsequent pages are never fetched, so their file links are never rewritten into Gatsby nodes, causing broken references for any media beyond the first chunk. Update both functions to fetch and return every page of matching items—no page should be left unprocessed.

fix(wordpress): ensure all file links are rewritten (#32679)


## Bug: `gatsby-remark-copy-linked-files` constructs invalid asset URLs and “undefined” destination paths

### Current behavior
- When a site uses `assetPrefix` with an **absolute URL** (e.g. `https://cdn.example.com`) the plugin produces malformed URLs for copied assets—typically mangling the protocol and generating a path like `/https:/cdn.example.com/…` instead of `https://cdn.example.com/…`.
- The plugin builds the destination file path inside `public` from the linked file’s hash and name, but if the file GraphQL node lacks `internal.contentDigest` or `name` the resulting path literally contains the string `"undefined"` (e.g. `public/undefined/undefined.gif`). The same `"undefined"` leaks into the final HTML URLs.

### Expected behavior
1. **Correct URL assembly when `pathPrefix` is supplied**
   (`pathPrefix` is the value Gatsby passes to the plugin, already combining any base path and `assetPrefix`.)
   - If `pathPrefix` is a **relative path** like `/blog`, the final URL must be:
     `/<pathPrefix>/<destination>` (e.g. `/blog/abc123/image.png`).
   - If `pathPrefix` is an **absolute URL** like `https://cdn.example.com` or `https://cdn.example.com/assets`, the final URL must preserve the full scheme and authority:
     e.g. `https://cdn.example.com/abc123/image.png`
     e.g. `https://cdn.example.com/assets/abc123/image.png`
   - No double slashes should appear (assuming conventional prefix values without a trailing slash), and the protocol must not be broken.

2. **Deterministic, meaningful destination paths**
   The destination path under the `public` directory (and the corresponding URL component) must be derived from the file node’s actual `internal.contentDigest` and `name`:
   - Default format: `<contentDigest>/<file-base-name>.<extension>` — no literal `"undefined"` shall appear.
   - When `options.destinationDir` is a string or function, the resulting path must still use the real hash and file name (never the string `"undefined"`).

3. **Backward compatibility**
   The fix must not break existing setups that use relative `pathPrefix` or custom `destinationDir`. All previously working URLs should remain identical.

### Acceptance criteria
- Given an absolute `assetPrefix`, the plugin’s output HTML contains fully qualified, correct URLs for every copied asset (images, documents).
- Given any valid `pathPrefix`, the generated `<img src="…">` (or equivalent link) matches the pattern `{prefix}/{contentDigest}/{name}.{ext}` with no stray characters or malformed parts.
- Running a build where the linked file node has a real `contentDigest` and `name` no longer produces paths containing `undefined`.
- Custom `destinationDir` callbacks receive the real file `hash` and `name` (and the resulting destination accurately reflects them).

fix(gatsby-remark-copy-linked-files): respect assetPrefix (#26976)


As a Gatsby developer, I want runtime errors during page loading to surface clear, actionable messages instead of vague fallbacks, so that I can quickly diagnose and fix issues caused by missing or blocked resources (component chunks, static query results) in both development and production.

**Acceptance Criteria:**

1. **Missing component chunk errors**
   - When a page’s component chunk cannot be found (e.g., the webpack chunk is missing or fails to load), the error thrown must explicitly include the chunk name (e.g., `"We couldn't find the correct component chunk with the name …"`).
   - This applies to both development (`DevLoader`) and production (`ProdLoader`) runtime paths.
   - In development, a missing chunk must throw immediately rather than silently resolving to `null`.

2. **Static query load failures**
   - If a static query result file (e.g., `/page-data/sq/d/*.json`) fails to load, the error must identify the failed resource by its hash and path (e.g., `"We couldn't load …/page-data/sq/d/…json"`).
   - This message must be surfaced when the static query batch request fails, not hidden behind a silen t fallback.

3. **Production error propagation**
   - When the production app cannot load page resources (`publicLoader.loadPage` returns an error status), the existing generic `"page resources for … not found"` message should be logged via `console.error`, **and** if the underlying error is available (e.g., a caught component or static query error), it must be re-thrown so that it appears in the console and error reporting tools, instead of being replaced by the generic message.
   - This makes production debugging possible without requiring the user to reproduce the exact build conditions.

4. **No breaking change to load behavior**
   - The loader’s internal resource status handling must continue to support `PageResourceStatus.Error` correctly; pages that fail to load should still not render, but the error channel must contain the specific cause.
   - The existing page database (`pageDb`) should store the error object on the page entry to enable later retrieval.

5. **Edge cases**
   - If a component chunk fails with a thrown error (e.g., network error, syntax error), that error object (`err`) should be preserved and returned instead of being replaced by `null`.
   - The handling of blocked resources (e.g., intentionally missing static query data or component assets) must rely on the improved error messages to pinpoint which resource is blocked, without affecting the overall resilience guarantees.

feat(gatsby): improve error messages at runtime (#29970)


Collection route path derivation mishandles node field values that already contain forward slashes (e.g., slugs with directory-like structures, such as `/fire-and-powder/`). It produces double slashes and drops trailing slashes. Also, numeric field values are not stringified, which can break URL construction.

**Required behavior:**
- `safeSlugify` should split the node value on `/`, slugify each segment individually, and rejoin with `/`. This preserves slashes as path separators while still applying slugification within each segment.
- After substituting placeholders with the slugified values, the final path must have no consecutive slashes. For example, `blog/{MarkdownRemark.fields__slug}.js` with a field value of `/fire-and-powder/` must produce `blog/fire-and-powder/`, not `blog//fire-and-powder/`.
- Trailing slashes in the node value must be retained in the final URL.
- Numeric field values (e.g., `id: 1`) must be converted to the string `"1"` before any slugification.
- Collection page templates must receive the original, unmodified field value via `pageContext` (e.g., `fields__slug` should hold the exact slug string from the node, not a transformed version).

fix(gatsby-plugin-page-creator): Update Route API tests & Fix slash replacement (#27335)


The whistleblower submission endpoint currently trusts the client‑provided `request['receivers']` list without fully applying the recipient‑selection policies that the official client enforces. A modified client can therefore:

- submit to receivers not assigned to the context,
- omit receivers that are flagged as `forcefully_selected`,
- exceed `maximum_selectable_receivers` when `allow_recipients_selection` is true,
- send a custom subset when `allow_recipients_selection` is *disabled* (the official client always submits all configured receivers, or only the mandatory ones, depending on `select_all_receivers`),
- supply arbitrary recipients even when a questionnaire option’s `trigger_receiver` should completely override the recipient list (the official client computes the override from the last enabled field whose selected option defines a `trigger_receiver`).

Add server‑side validation into the submission path that exactly mirrors the client’s logic and rejects mismatched requests with `InputValidationError`.

**Required invariants**

1. **Triggered override** – Traverse the questionnaire steps in schema order, using the submitted answers and the `triggered_by_options` rules to decide which fields are enabled. Collect the `trigger_receiver` list of the *last* enabled option that declares one. If an override is triggered, the submitted `receivers` must match it exactly (order insensitive); otherwise the override is `None` and normal rules apply.

2. **Normal recipient‑selection rules** (no triggered override)
   - Determine the set of receiver IDs configured for the context (`ReceiverContext`) and the subset whose `User.forcefully_selected` is true.
   - If `context.allow_recipients_selection` is **false**: the submitted receivers must equal the set the official client would have defaulted to: all context‑configured receivers when `select_all_receivers` is true, otherwise only the forcefully‑selected receivers.
   - If `allow_recipients_selection` is **true**:
     - Every submitted receiver must belong to the context.
     - All forcefully‑selected receivers must be included.
     - The number of submitted receivers must be at least 1 and ≤ `maximum_selectable_receivers` (when > 0).

3. **Integration** – Insert the validation call right after the questionnaire steps are available and before the existing receiver lookup. Move the existing `maximum_selectable_receivers` check from its current location into the new validation so that all policy violations are raised together.

**Implementation notes**

- The submission already provides `identity_provided`; pass it because fields belonging to the whistleblower‑identity template are disabled when identity is not available.
- The steps list is obtained early from `db_get_questionnaire`. The answers follow the same `{field_id: [entry_dicts]}` structure used by scoring.
- You can factor the answer‑traversal and option‑selection logic out of `db_evaluate_answers_score` to reuse it both for scoring and for the override/validation walk. Ensure the original answers are not mutated by the validation (the score computation still needs them).
- The receiver‑context and user data can be queried with a joined query against `ReceiverContext` and `User`.

Validate the recipient selection server-side on submission


**Request: Fix Tor2Web login blocking and correct transport‑security error codes**

1. **Admin login must succeed over Tor2Web when permitted by configuration.**
   - Currently the login endpoint (`/login`) inspects the `X‑Tor2Web` request header and rejects the attempt even if the node configuration (`GLSetting.tor2web_permitted_ops`) allows the requested role to operate over Tor2Web. This makes it impossible for an admin to log in via a Tor2Web proxy when `tor2web_permitted_ops['admin']` is `True`.
   - Expected behavior: the login endpoint itself must **not** enforce transport restrictions. Credential validation should proceed normally regardless of the `X‑Tor2Web` header. Transport‑level enforcement belongs on the protected resource handlers, which already use the `transport_security_check` decorator with per‑role configuration.

2. **`TorNetworkRequired` must return HTTP 417, not 403.**
   - When a protected endpoint detects that the request came via Tor2Web but the role is not permitted for that transport, the existing `TorNetworkRequired` error should return **417 Expectation Failed** instead of 403 Forbidden. Update the error reason message to `"Resource can be accessed only within Tor network"`.

3. **Correct the documented status code in the `authenticated` decorator.**
   - The docstring of the `authenticated` decorator claims it returns a 401 error when the user is not authenticated. The actual `NotAuthenticated` exception uses **412**. Fix the comment to match the real status code so developers are not misled.

4. **Simplify the `authenticated` decorator to accept a single role.**
   - The decorator currently accepts `*roles` (a variadic argument), yet every handler that uses it passes exactly one role. Change the signature to accept a single `role` string and update the internal comparison logic accordingly. This prevents the misleading impression that multiple roles can be checked simultaneously and makes the decorator’s contract clearer.

5. **Remove stray debug prints.**
   - Remove any leftover `print` statements that log internal information (for example file paths) in request handlers. These can leak details in production logs.

**Acceptance criteria**
- An admin can log in with the correct password and `X‑Tor2Web: any` header **provided `GLSetting.tor2web_permitted_ops['admin']` is `True`**. The response contains a valid session ID.
- Access to a protected endpoint (e.g., a file download) with an `X‑Tor2Web` header when the role is **not** permitted for Tor2Web still fails, but now the response status is **417** and the body carries the updated reason.
- The login endpoint does **not** reject valid credentials based on the presence or absence of the `X‑Tor2Web` header; all transport gating is performed by the `transport_security_check` decorator on the relevant handler methods.
- The `authenticated` decorator’s docstring accurately reflects that `NotAuthenticated` yields a 412 status.


**Title:** Inconsistent admin test‑mail endpoint – should be part of unified operation API

**Description**

The admin panel can send a test email to verify SMTP configuration. Right now that action uses a dedicated
endpoint (`POST /admin/notification/mail`), while other admin actions (set hostname, reset submissions,
disable 2FA, etc.) are performed through a single, unified operation API at `PUT /admin/config`.

This means the client‑side code must handle the test‑mail action differently from every other admin
action, and the backend has an extra handler and route that follows a different pattern.

**Expected behaviour**

- The test‑mail action is invoked the same way as all other admin operations: a `PUT` to
  `/admin/config` with a JSON body containing `"operation": "test_mail"` and an empty `args` object.
- The old `POST /admin/notification/mail` endpoint no longer exists.
- The client‑side JavaScript must be updated so the “send test mail” button calls the new
  unified operation instead of the old endpoint.
- The operation must be restricted to admins (as the current handler is). No other role should be
  able to trigger it.
- The actual mail‑sending logic (fetching the admin user, building the test template, and sending
  via `sendmail`) stays the same; only the entry point changes.

**Impact**

Without this change we have a split API surface for admin operations, which makes the front‑end
code more complex and creates unnecessary maintenance on the backend.

Uniform api for testing mail notifications to operation api


```
Unify IP-filter rejection with generic authentication failure

Currently, when authentication fails because the client IP address is not permitted
by the tenant's IP filter, the server returns a distinct error
(`AccessLocationInvalid`). This allows an attacker to distinguish that the failure
is due to network location rather than bad credentials, and to probe permissible
IP ranges.

To prevent information leakage, change the IP filter check so that it raises the
same `InvalidAuthentication` error that is used for incorrect passwords. After the
change, any authentication rejection—whether the cause is a password mismatch or an
IP policy violation—must produce an identical error response to clients.

The now-unused `AccessLocationInvalid` error class should be removed.
```

Remove AccessLocationInvalid error in favor of InvalidAuthentication


**Title:** Move expiration reminder emails from Cleaning to Notification scheduler

**Context:**
Currently the daily `Cleaning` job also generates expiration summary emails for recipients when submissions are about to expire. This mixes data-housekeeping with notification logic, making the system harder to maintain and bypassing the normal daily notification pipeline.

**Desired behavior:**
- The `Cleaning` job must **only** perform cleanup (expiring tips, deleting files, managing demo platforms). It must not generate, queue, or send any notification emails.
- Expiration summary emails (type `tip_expiration_summary`) should be generated by the `MailGenerator` inside the `Notification` job, during the same cycle that produces unread reminders and other scheduled notifications.
- The new location must respect the same business rules as before:
  - Use the tenant's `tip_expiration_threshold` to determine which tips are “near expiry”.
  - For each recipient with expiring tips, build the same template data as the old implementation (node, user, `expiring_submission_count`, `earliest_expiration_date`).
  - Skip recipients who have disabled notifications (`user['notification']` is false).
  - For tenants in `default` mode, fetch the notification configuration from the tenant itself; otherwise from tenant ID 1.
  - Queue the mail via `Mail` model with the correct `address`, `subject`, and `body`.

**Acceptance criteria:**
- When `Cleaning` runs daily and tips are close to expiration, **no** expiration summary emails appear in the `Mail` queue.
- When `Notification` generates the daily batch and tips are near their expiration threshold, expiration summary emails **are** produced for eligible recipients, side-by-side with other notification types.
- Existing notification preferences (enabled recipients, silent tenants, etc.) are correctly observed for expiration emails just as they are for other notification types.
- Unread report reminder emails continue to work unchanged.
- The cleaning logic remains otherwise untouched (expired tip cascade deletion, file secure deletion, demo platform expiry).

**Non-goals:**
- No change to the template structure, email formatting, or keyword substitution logic.
- No change to the threshold calculation.

Refactor cleaning job moving notifications to notification sched


When a recipient updates a report's status via the RTip `update_status` operation, the server currently accepts any `status_id` and `substatus_id` string without verifying that they match tenant‑configured submission statuses. This allows callers to supply arbitrary or non‑existent identifiers, leading to an inconsistent database state that is not subsequently surfaced as a client error.

We need the operation to reject requests that reference status or substatus values not configured for the tenant:

- The `status_id` must exist in the tenant’s `SubmissionStatus` records.
- If a `substatus_id` is provided (non‑empty / not `None`), it must exist in the tenant’s `SubmissionSubStatus` records **and** that substatus must belong to the requested `status_id`.
- The special `'new'` status that is currently handled as a no‑op should continue to be handled before any validation, so that `status_id == 'new'` does not require configuration checks.

When a status or substatus cannot be found, the server should raise an appropriate error that results in a 404‑class response (No Result Found). The rest of the status‑change logic — including the close/reopen handling — must only execute once the identifiers are confirmed valid.

No changes to the persistence of the status itself are required; only a gate that prevents untethered identifiers from being written.

Validate report status transitions against the tenant configured statuses


### Request

Automatically remove now‑unsupported languages from the database during upgrade

When GlobaLeaks starts and detects that it must apply data updates (because the stored version is older than the running version), it should also clean up any enabled languages that are no longer in the current set of supported languages (`LANGUAGES_SUPPORTED_CODES` from `globaleaks/__init__.py`).

Specifically:

- For every language code present in the `enabledlanguage` table that is **not** in `LANGUAGES_SUPPORTED_CODES`:
  - Remove that language’s row from `enabledlanguage`.
  - Delete **all** rows in `config_l10n` that belong to that language (i.e., where `config_l10n.lang` equals the removed language code).
  - Log a human‑readable warning message for each dropped language, indicating that its translations are no longer supported.

- This cleanup must happen **after** the existing `db_update_appdata` step and **before** the system re‑applies default localization settings for the currently supported languages (i.e., before the call(s) that update localized defaults). This ordering ensures that stale localised settings from removed languages are not accidentally re‑populated.

- The removal must be performed in a single database‑update routine when the system migrates data after a version change; it does **not** need to happen at any other time (such as during normal runtime after a configuration change).

Test for and remove languages dropped from the supported set


Feature request: Include server start time in the public API node response.

**Motivation**
Consumers of the public API (e.g., monitoring tools, client dashboards) currently have no way to determine how long the GlobaLeaks instance has been running or when it was last restarted. Exposing the server process start time would allow these tools to detect restarts and track uptime without requiring administrative access.

**Desired behavior**
Add a new field `start_time` to the `node` object returned by the endpoint that serves public resources (the same endpoint that returns node configuration, contexts, and receivers). The value should be the timestamp of when the server process (or application state) was initially created, not the time of the current request. It should be an ISO 8601 date-time string and should remain stable for the lifetime of the running process.

**Additional notes**
- The response schema for the node object needs to be updated to include the new field, otherwise validation may reject the response.
- If the timestamp is a Python `datetime` object, make sure it is serialized correctly to JSON (e.g., as a string) so that the endpoint does not fail when building the response.


**Silence all log output during unit tests**

The unit test suite currently produces undesirable log output. Specifically:

- Database migration code writes information directly to stdout via `print`, which bypasses the existing log‑silencing mechanism and clutters test runner output.
- The existing silencing mechanism creates a `./test.log` file on disk, which is unnecessary and can cause test pollution.

**Acceptance criteria**
- Running any unit test suite **must produce zero log output on stdout/stderr** (except for unavoidable framework‑level errors unrelated to application logging).
- No log files (e.g., `test.log`) are created during test execution unless explicitly requested by a developer.
- All application‑level logging that was previously silenced (debug, info, error) remains suppressed – no regression in the old behaviour.
- Production logging code (especially in database migrations) **must not use raw `print`** – it must use the project logger (`globaleaks.utils.log`) so that silencing applies uniformly.

**Constraints / Safety**
- The silencing approach should work even if new logging calls are added to production code – it should not require enumerating every log level or method.
- Do not alter the public API of the production logger in a way that would affect runtime deployments.
- Ensure the test silencing logic is self‑contained in the test helper layer and does not leak into production code paths.

Revise implementation of log silencing during unit tests


**Title:** Defang URLs and email addresses in support request emails; use server hostname for Site field

**Description:**
The support request email sent to administrators currently includes raw, untrusted user input: the `From` email address, a `Site` URL provided by the user, and the free-text request body. An administrator who views this email may inadvertently click a malicious link or spoofed address, creating a phishing risk.

We should apply URL/email “defanging” (inserting brackets around `://`, `:`, `.`, `@` to make them non-clickable but human-readable) to all untrusted fields in the support email. Defanging must:

- defang both email addresses and typical URL schemes (http, https, ftp, etc.)
- be **idempotent** – text that is already defanged should not be double-defanged (e.g. `[.]` must stay `[.]`)
- strip **invisible/zero-width Unicode characters** that could be used to bypass simple filters (e.g. U+200B, U+200C, U+200D, U+FEFF)
- handle mixed content where multiple URLs, emails, and plain colons/dots appear in the same text
- not defang the fixed `Site:` header or any other system-generated part of the email

Additionally, the `Site` field currently uses a user-supplied URL value (`request['url']`). An attacker could set this to an arbitrary domain, spoofing the origin. **Change the `Site` field to use the server’s actual hostname** (the `Host` header from the incoming request, or equivalent) so that it always reflects the true Globaleaks instance, not a user-controlled string.

This change affects the `/support` endpoint. The resulting email body should format the defanged information in a way that remains fully readable but removes the risk of accidental click-through.

**Acceptance criteria (behavioral):**
1. Email: `From: user@example.com` → `From: user[@]example[.]com`
2. A URL like `https://attacker.com` in the request text becomes `https[://]attacker[.]com`
3. Already defanged input like `foo[.]bar` stays `foo[.]bar`, not `foo[.][.]bar`
4. Zero-width or invisible characters are removed before any brackets are inserted
5. The `Site:` line shows the server’s hostname (e.g. `globaleaks.example.org`), not a user-supplied string

Implement URL defanging on untrusted support user input (#4794)


**Title:** Implement periodic password change enforcement for user accounts
**Type:** feature
**Component:** authentication, scheduled jobs, admin configuration
**Priority:** medium

### Description
Currently the `password_change_period` configuration key exists (default `-1`) but is not enforced anywhere. Users are never forced to change their passwords after account creation or a prior password change, even though the `User` model already carries `password_change_needed` (a boolean flag) and `password_change_date` (timestamp of last change).

We need to activate this facility so that administrators can require users to rotate their passwords after a configurable number of days.

### Requirements

1. **Configurable threshold**
   - The existing `password_change_period` node config (integer, in days) should control the behaviour.
   - A value of `0` must disable enforcement entirely (password never expires).
   - The default value should be changed from `-1` to `90` days.

2. **Daily expiration job**
   - Introduce logic in the daily maintenance loop (currently `backend/globaleaks/jobs/daily.py::Daily.operation`) that evaluates all users across every tenant.
   - For each tenant with a positive `password_change_period`, compute a cutoff date: `now - period`. Any user whose `password_change_date` is **before** that cutoff (including users who have **never** changed their password, i.e. `password_change_date` is null) must have their `password_change_needed` flag set to `True`.
   - The check must be performed inside a database transaction (the existing pattern uses `@transact` on helper methods).

3. **Tenant isolation**
   - The expiration logic must iterate over all tenants (`self.state.tenant_state`) and apply the per-tenant config value found in `State.tenant_cache[tid]`.
   - Users must be scoped to the tenant via `tid`.

4. **Admin UI exposure**
   - The “Advanced” settings panel in the administration interface should expose an input for `password_change_period` (type number, minimum 0). The field label should indicate the number of days before a password change is required, with a note that a value of 0 disables the feature.
   - The panel already uses `data-ng-model="admin.node.password_change_period"`; ensure the input is added to the appropriate form group.

5. **Test infrastructure updates**
   - The mock config `MockDict` should reflect the new default of 90 (currently `-1`).
   - Provide a reusable helper in the test base class that can put one user (e.g. `admin`) into a “password expired” state while leaving other users fresh, so that tests can verify the daily job activates only the intended targets.

6. **Safety**
   - The job must be a well-behaved part of the existing daily `operation` coroutine, not a separate background process.

### Acceptance criteria (implied)
- With `password_change_period = 90` and an admin whose password was last set more than 90 days ago (or never set), running the daily job flips `password_change_needed` to `True` for that admin.
- Other users whose passwords are recent remain unchanged.
- Setting the period to `0` causes no password expiration.
- The admin UI saves the value correctly and it is reflected in the node configuration.

Implement password reset over time period logic


When a receiver exports a tip (or downloads a large receiver file, or fetches a large static file), the GlobaLeaks service becomes unresponsive for all other users until the response finishes. This happens because the file‑serving loops (`write_file` in the base handler, the ExportHandler, the Download handler, and the static‑file handler) all block the Twisted reactor while writing the response body.

We need to make those handlers cooperative:

- The file‑writing logic should yield control to the reactor after each chunk so that other requests can be processed.
- For the tip‑export handler the zip‑stream generation itself can be heavy; move chunk assembly into a thread to keep the main reactor free.
- Increase the streaming chunk size (`GLSettings.file_chunk_size`) from 8 KiB to 1 MiB — that’s large enough to avoid too many reactor round‑trips but still keeps the server responsive.
- Ensure file handles are closed reliably even when the streaming is interrupted or fails.
- The export handler’s test needs to be updated so that `flush()` can be mocked during the test run.


We store password reset tokens as raw filenames under the ramdisk directory. That means a sensitive secret (the token) is written into the filesystem namespace, where it could be leaked through directory listings, file monitoring, or local file-inclusion vulnerabilities. The token should be hashed (for example with SHA‑256) before it is used to construct a file path, so the filename never contains the plaintext token. All code paths that write or read a token‑based filename on the ramdisk must use the same hash scheme, keeping the actual token hidden even from other processes that can see the ramdisk.

Hash password reset tokens before storing them on disk


**Summary:** Refactor the GPG subsystem to use per‑operation temporary keyrings, add support for encrypting delivered files, and fix the option‑parsing bug that prevents enabling file encryption independently.

---

**Background**

- The notification plugin already encrypts email bodies for receivers that have GPG enabled.
- The current implementation uses a single global GPG home directory (`GLSetting.gpgroot`) for all key imports and encryption operations. This is not safe under concurrency and can leak state across receivers.
- File delivery (`jobs/delivery_sched.py`) always stores the plaintext path in `ReceiverFile.file_path`; there is no encryption even when a receiver has opted into file encryption.

**What needs to change**

1. **Isolated keyrings**
   Every GPG operation (key validation, message encryption, file encryption) must use its own temporary GNUPG home directory. The temporary directory must be cleaned up after the operation, even if an error occurs.

2. **File encryption during delivery**
   When the delivery job creates a `ReceiverFile` record for a receiver who has:
   - `gpg_key_status == Receiver._gpg_types[1]` (Enabled) **and**
   - `gpg_enable_files == True`
   the plaintext file must be encrypted using the receiver’s public GPG key, and the encrypted content stored as `receiverfile.file_path`.
   If encryption fails, the system must log the error and **not** fall back to storing the plaintext (to avoid a downgrade attack).
   Use the same GPG helper as the notification plugin, so the behaviour is consistent.

3. **Fix option parsing**
   In the function that saves GPG settings from a receiver update request (`globaleaks/security.py`), the field for file encryption is mistakenly set from the `gpg_enable_notification` key instead of `gpg_enable_files`. This must be corrected so that a receiver can independently enable file encryption.
   The same function must also clear `gpg_enable_files` and `gpg_enable_notification` when the GPG key is removed.

4. **Unify GPG helpers**
   Replace the existing free functions (`gpg_encrypt`, `import_gpg_key`, `gpg_clean`, `gpg_options_manage`) with a single, stateful object that manages the temporary keyring, validates the receiver’s key, encrypts messages and files, and disposes of the temporary directory.
   All call sites (notification plugin, delivery job, admin/receiver handlers) must use the new helper.

**Edge cases / constraints**

- The helper must not leave orphaned temporary directories on disk under any circumstance.
- Key validation must succeed before any encryption attempt; `GPGKeyInvalid` must be raised if the key cannot be imported or its fingerprint cannot be confirmed.
- The encrypted output of file encryption must be a valid PGP message (armored), not a file path. The delivery job and notification plugin must treat the result accordingly.
- No plaintext fallback on encryption failure.

start refactor of GPG, now supported https://github.com/globaleaks/GlobaLeaks/issues/275


We need to change the default tor2web (HTTPS) access permissions so that admin, custodian, receiver, and unauthenticated users can access the platform over HTTPS by default. Currently only admin and unauthenticated get `True` while custodian and receiver get `False`; the new default should be `True` for all except whistleblower (which remains `False`).

In addition, the wizard currently automatically modifies the admin tor2web setting based on whether the admin connection is detected as anonymized (Tor). Because the default will now be `True` regardless, that automatic override logic should be removed so the admin’s setting is not changed after a clean install.

As a clean-up, the old `defaults` dictionary in `GLSettingsClass` that held duplicate versions of these tor2web defaults (and also `authentication_lifetime`) should be removed. The direct attribute `authentication_lifetime` should be used consistently wherever the old dict was accessed (including in session-creation and memory-refresh code), and the memory-copy initialization should be simplified. This will eliminate the duplication of defaults between the settings object and the Node model fields. All other default values that were only in the `defaults` dict can be dropped because they were already duplicated elsewhere or no longer used.

Change admin/custodian/receiver default HTTPS access to True


The admin files overview endpoint (`GET /admin/overview/files`) currently returns only `InternalFile` records. It should also include `ReceiverFile` records, since these represent per‑receiver copies of encrypted files that exist independently on disk.

Per‑`ReceiverFile` entry must contain:
- the parent internal file’s `id`, `name`, and `content_type`
- the receiver file’s own `size`
- the associated internal tip id (`itip`)
- the receiver file’s creation date
- an integer field `rfiles` set to `1`
- a `stored` boolean and a `path` field indicating whether the file is actually present on disk

The response schema must be updated to include this new `rfiles` integer field. Orphan files on disk that are not referenced by **any** internal or receiver file must still appear in the overview.

FilesOverview now correctly keeps track of receiversfiles


Simplify the file-permission and privilege-drop operations currently attached to `SettingsClass`. The methods `fix_file_permissions` and `drop_privileges` on the settings class bind safe-startup concerns to the configuration object; extract them into standalone, parameterised utility functions so they can be reused without a settings instance. The recursive permission fix should accept path, uid, gid, a directory mode, and a file mode, and it must correctly apply the appropriate mode to the root path itself (the old method accidentally skipped the root when it matched the working directory). The privilege drop can be stripped of the old guard-printing and process exit on failure; just call `os.setgid`/`os.setuid` and let the OS raise on problems. Remove the now-unused `remove_directories` method from `SettingsClass`; the test helper that calls it should use `distutils.dir_util.remove_tree` directly instead. Ensure the backend startup in `Service.startService` uses the new utilities with the same paths and ownership parameters.


```
During tip cleaning (after a tip expires), all associated files must be removed from
the file system. Some receiver files can point to the same physical file on disk as
their parent internal file (i.e. `ReceiverFile.file_path` equals
`InternalFile.file_path`). In that situation the current cleaning logic tries to
remove the same path twice: once when cleaning the internal file and again when
cleaning the receiver file. This produces an `OSError` (and a logged error) because
the file no longer exists, and may leave the cleaning process in an inconsistent
state.

Acceptance criteria / expected behavior:

1. When cleaning an expired tip, each unique on-disk file must be removed exactly
   once, even when a receiver file’s `file_path` matches the `file_path` of the
   internal file it belongs to.
2. The cleaning operation must not log an "Unable to remove" error or raise an
   `OSError` because a file was already deleted in the same cleaning cycle.
3. The fix must correctly handle the case where only some receiver files share
   a path with the parent internal file while others reference distinct files
   (those distinct files must still be removed).
4. The cleaning logic should associate receiver files with their corresponding
   internal file (using the existing `ReceiverFile.internalfile_id` relationship)
   so that path comparisons are scoped per internal file, rather than globally
   across all receiver files of the tip.
5. No other tip cleaning behaviour (e.g. removing database records) should change;
   only the file-system removal step is affected.
```

fixed cleaning when internalfile.file_path equals a corresponding receiverfile.file_path.


```
Title: Serve index.html with correct language and text direction attributes

Description:
When a user accesses the GlobaLeaks web application, the server returns the main HTML page (index.html). Currently this page hardcodes `lang="en"` and `dir="ltr"` on the `<html>` element, regardless of the client's language preference.

This is problematic for users whose language is right-to-left (RTL), such as Arabic, Hebrew, Persian, Divehi, Pashto, Uyghur, Urdu, etc. They receive a page marked as left-to-right, which can break layout and accessibility.

We need the server to:
- Replace the `lang` attribute with the language code negotiated for that request.
- Replace the `dir` attribute with `"rtl"` if the language is known to be written right-to-left, otherwise `"ltr"`.

The set of RTL languages among GlobaLeaks' supported languages must be defined in the codebase. The server already determines the request language (via Accept-Language header or URL) and makes it available during request processing. The index.html is processed before being sent to replace a CSP nonce; the lang/dir replacement should happen alongside that processing.

Ensure that all supported languages, including those without explicit RTL handling, default to `dir="ltr"`.

Acceptance criteria:
- index.html served for any supported language contains the correct `lang` and `dir` attributes.
- RTL languages (at minimum `ar`, `dv`, `fa`, `fa_AF`, `he`, `ps`, `ug`, `ur`) get `dir="rtl"`.
- No changes to client-side code; server-side only.
```

Serve index.html with the requested language and text direction


```
Title: Fix operation dispatch in receiver tip handler (RTipInstance)

The receiver tip instance endpoint (`/rtip/{tip_id}` PUT) supports multiple operations via an `operation` field in the JSON body. The current implementation has several issues:

1. Operation name mismatch: the handler maps operation names that do not match what the client sends. For instance, `postpone` vs `postpone_expiration`, and the label-setting functionality is overloaded into a generic `set` operation instead of using a distinct `set_label` operation.

2. Dispatch and binding: the base handler class used for multi-operation endpoints (`OperationsHandler` in `base.py`) builds its operation table using bound methods (e.g., `self.postpone_expiration`), but then calls them in a way that can cause incorrect argument passing or method resolution.

3. Argument validation gap: when an operation expects no arguments (e.g., `postpone_expiration`), the dispatcher does not handle the `None` argument schema correctly and may skip validation altogether.

**Desired behavior:**
- The base handler (consider renaming it to `OperationHandler` to reflect its purpose) should look up the operation descriptor from the `operation_descriptors` dictionary, validate the `args` payload using the provided schema (if any), and invoke the corresponding method with `self` passed explicitly.
- The `RTipInstance.operation_descriptors` should return:
  - `'postpone_expiration'` with no argument schema.
  - `'set'` with a schema expecting `key` and `value` (used for toggling tip-level flags like enable_two_way_comments etc.).
  - `'set_label'` with a schema expecting `value` (a separate operation for setting the tip label).
- The operation names must align with what the front-end test suite expects.

**Acceptance criteria:**
- PUT requests to the RTip instance with `"operation": "postpone_expiration"` correctly postpone the expiration date.
- PUT requests with `"operation": "set_label"` correctly set the label.
- The generic `"operation": "set"` still works for boolean toggle keys.
- All argument validations are applied (or skipped when the descriptor is `None`).
- The handler methods receive the proper `self` reference.
```

Apply review of commit f76076121d140a612fea11b92a0c60058902db9b


**Request:**
When a submission token has already been consumed (or is otherwise absent from the runtime token store), the API currently returns an HTTP 500 `Internal Server Error` because a bare `KeyError` propagates from the token‑pop operation. Return a domain‑specific `SessionExpired` error (HTTP 401) instead, so clients can distinguish an expired/reused token from a server fault.

**Required behaviour:**
- In the submission handler (`SubmissionInstance.put`), when `self.state.tokens.pop(token_id)` returns `None` (token missing), raise a new `SessionExpired` exception.
- Define `SessionExpired` as a subclass of the existing `GLException` in the REST errors module. Give it a unique numeric `error_code` (the next available in the error‑code sequence), `status_code = 401`, and `reason = "Session expired"`.
- The existing test that verifies token reuse blocks must expect `SessionExpired` rather than `KeyError`.

Add specific exception to manage expired user sessions


**Bug: Tor detection broken via HTTPS due to header case mismatch; CSP syntax and missing header checks**

**Observed behavior**
- When a user connects over Tor using HTTPS, the client-side logic that detects Tor and offers to redirect to the `.onion` address never activates, even though the backend sets a header to signal Tor usage.
- The `Content-Security-Policy` response header appears to be missing a required semicolon after `frame-ancestors 'none'`, which might cause CSP parsing failures.
- Existing validation of security-related response headers is incomplete: several headers that the backend is supposed to inject are not being verified, including `X-XSS-Protection`, `X-Check-Tor`, and a complete `Content-Security-Policy` value, making it easy for regressions to go unnoticed.

**Investigation so far**
The backend header that indicates Tor usage is currently being sent with the name `x-check-tor` (all lowercase). The client-side code that reads response headers to decide whether to show the onion-service redirect expects the header name `X-Check-Tor` (mixed case). This mismatch causes the condition to always be false, so the redirect prompt never appears.

**Expected behavior**
- Tor detection must work reliably over HTTPS: the header name sent by the backend should match exactly what the client expects (and the comparison should be robust).
- The `Content-Security-Policy` value should be syntactically correct; `frame-ancestors 'none'` must be followed by a semicolon before the closing quote.
- All security-related response headers that the backend injects should be present with the correct values. This includes, but is not necessarily limited to: `Content-Security-Policy`, `X-Frame-Options`, `X-Content-Type-Options`, `X-XSS-Protection`, `X-Check-Tor`, `Referrer-Policy`, `Cache-Control`, `Pragma`, and `Expires`. The order and exact values matter for security and should be stable.

**Impact**
- Tor users over HTTPS never get the intended guidance to switch to the more secure `.onion` address.
- A malformed CSP header may weaken or break the content security policy in some browsers.
- Missing header checks allow accidental changes that could degrade the application’s security posture without detection.

Improve testing of injection security headers


**Title:** Deleting a field does not recursively delete its children and leaves orphaned steps

**Description:**
When a field (especially a group field) is deleted through the admin API, its child fields are not removed from the database. This creates orphaned child records that remain in the system with no valid parent.

Additionally, if the field is directly referenced by a step (i.e., it is assigned to a form step within a context), that step row is not deleted. After the field is removed, the step points to a non-existent field ID, causing broken references that lead to errors when the step is later accessed or when a context’s field tree is processed.

**Expected behavior:**
- Deleting a field should recursively delete all its child fields, ensuring no orphaned children remain.
- If the field is directly linked to a step (a record that ties a context, a field, and a step number), that step must also be deleted when the field is removed.
- The deletion should still raise a “FieldIdNotFound” error if the provided field ID does not exist.

**Steps to reproduce:**
1. Create a field that acts as a group (has multiple child fields assigned via the field–field relationship).
2. Assign at least one of those fields directly to a step in a context.
3. Delete the parent field via the admin field deletion endpoint.
4. Observe that the child fields persist in the database and the step record still references the now-deleted field.

**Impact:**
- Data integrity is compromised with orphaned fields and steps.
- Subsequent operations that enumerate fields or steps may encounter unexpected exceptions due to invalid references.
- Context field trees may become inconsistent.

Field deletion shall be recursive, and include steps whenever is the case.