Repository navigation
v1.0.10
A 1.0 patch release with security fixes, including the fixes from 1.1.1 to 1.1.5 that the 1.0 line did not have; upgrading from 1.0.9 is recommended. The 1.0 line is no longer maintained, so moving to 1.2 or later is advised. Review these behaviour changes before upgrading:
- Live transports (WebSocket, SSE, the HTTP POST fallback, the API,
{% live_render %}children) refuse a view whose access check lives only in its ownsetup(),dispatch(),get()orpost(), in a decorator on one of them or in a URLconf wrapper, until the view declares its rule (login_required,permission_requiredorcheck_permissions()). On this line the system check that names those views isdjust.S004, the id it shares with theDEBUG/ALLOWED_HOSTScheck. - With Django's
LoginRequiredMiddlewareinstalled, live mounts get the answer the middleware gives. - djust admin: staff users need the model's Django permissions (superusers have them all).
- The
/_djust/observability/endpoints need theX-Djust-Observability-Tokenheader (manage.py djust_observability_tokenprints it; the djust MCP server sends it). - Strings passed to component slots and attributes are escaped unless marked safe, as in 1.1.4.
- Templates: the bare
{{ value|linebreaks }}renders real<p>/<br>tags around escaped text,{{ value|escape|F }}for a plain filterFdouble-escapes, and a custom filter registered withis_safe=Trueescapes plain input. - A view whose
mount()raises is no longer left mounted; aPermissionDeniedthere closes the socket with 4403.
Security
-
The live mount checks object permission after
handle_params(). The WebSocket mount ranhas_object_permission()aftermount()but beforehandle_params(), so a view that selects its object from the URL's query parameters inhandle_params()was checked against the objectmount()chose and then rendered the one the parameters selected. The mount through the SSE message endpoint (ViewRuntime.dispatch_mount) ran no object check at all. Both now runmount(),handle_params(), the object check, and then the first render: the order the HTTP GET render andurl_changenavigation already used. A refused WebSocket mount still sends its error frame and closes with 4403; a refused SSE message mount sends apermission_deniederror and mounts no view. -
Live transports enforce or refuse access control that only the HTTP request runs.
The WebSocket and SSE mount, events, the HTTP POST event fallback, the API
and{% live_render %}children never call a view'ssetup(),
dispatch(),get()orpost(), and never run the wrappers the URLconf
puts aroundas_view(). djust replays its own declarations and Django's
LoginRequiredMixin,PermissionRequiredMixinandUserPassesTestMixin
there, but a customAccessMixinsubclass that checks the user in its own
dispatch(), a@method_decorator(...)ondispatch,getorpost, an
overridden method that checks the user, or a URLconf wrapper such as
path("x/", login_required(MyView.as_view()))did not run at all.
check_view_authnow:- replays Django's
login_required,staff_member_required,
user_passes_test(...)andpermission_required(...)applied with
@method_decorator, wrapped aroundas_view()in the URLconf (read from
the server's URLconf, not from the URL the client sends) or by an
as_view()override, and other packages' decorators built on
user_passes_test. The same users are admitted as on the HTTP request;
an authenticated user who fails the test getsPermissionDenied, where
Django's HTTP response would redirect to the login page. Async test
functions are awaited (called directly on the event loop's thread, the
check refuses instead of blocking).login_not_requiredis not access
control. A decorator applied only topostis checked for every event on
the page (the view's, its components', its embedded views' and uploads),
not for the mount; - refuses a view whose custom
AccessMixinsubclass definesdispatch(),
unless it declarespermission_requiredorcheck_permissions()
(login_required = Truealone does not cover it); - refuses a view whose
setup()/dispatch()/get()/post()override, a
decorator on one (also under another decorator stacked above
@method_decorator, or returned by a project decorator that delegates to
it), or a URLconf wrapper checks the user (any attribute of it, such as
is_staff,groupsor a role) and denies by any means (raises, including
PermissionDeniedandHttp404; callshandle_no_permission()or
redirect_to_login(); returns a redirect, an error response or a page of
its own such asrender(...)orHttpResponse(...); calls
get_object_or_404()), unless a declared rule covers it (login_requiredcovers a check that reads only
is_authenticated/is_anonymous); - does not refuse a view for any other decorator or URLconf wrapper that
is neither Django's nor djust's and whose code does not check the user (a
CSP, rate-limit or logging decorator); it does not run on live
transports.
Permitted users of views using Django's
login_required,
staff_member_required,user_passes_testandpermission_required
decorators and wrappers, or its three auth mixins, are unaffected. Views
whose access check lives in their ownsetup(),dispatch(),get(),
post(), decorators or URLconf wrappers are refused for every user until
the rule is declared. The HTTP GET page, which Django's dispatch already
gated, is unchanged. System checkdjust.S004(the dispatch-auth check; an Error, so
manage.py checkandmigratestop until the view is fixed; on this release
line the id is shared with the DEBUG/ALLOWED_HOSTS check) names each refused
view defined in the project. Overrides that only read the user, unknown
decorators and wrappers, refused views that an installed package defines and
URLconf routes whose wrapper does not usefunctools.wrapsare not
reported.djust.S004no longer reports Django's own decorators on
dispatch(), which are replayed. Onlydjustand
djust.*modules are treated as framework code: apps and packages named
djust_*are inspected, djust-auth'sLoginRequiredLiveViewMixinis read
aslogin_required = True, and djust-tenants'TenantMixinis not
inspected.LoginRequiredLiveViewMixinindjust.authnow also sets
login_required = True.
Upgrade note: a view refused this way must declare its rule;
login_required = Falseis not a declaration. - replays Django's
-
MCP tools keep filesystem paths inside the project.
find_handlers_for_templateread any absolute path that existed, so it
confirmed which files existed anywhere on disk and returneddj-*-shaped
strings from them. It now reads an absolute path only when the file resolves
(symlinks and..followed) inside a directory the template loaders serve or
insideBASE_DIR, and answers any other absolute path like a missing
template.seed_fixturescompared paths with a string prefix, so
/app-datapassed for aBASE_DIRof/app, and skipped the check when
BASE_DIRwas unset; it now compares whole path components and refuses to
load anything withoutBASE_DIR. Both use one containment helper. -
Component content is escaped on every render path (1.0 backport of the 1.1 component escaping for these components).
MasonryGrid/{% masonry_grid %}itemcontentandDashboardGrid/{% dashboard_grid %}panelcontentare escaped by the component class, the Django template tag and the Rust-engine handler;AuditLog/{% audit_log %}escape column names in each cell'sclassattribute;{% file_dropzone %}escapesnamein the element id;Meter/{% meter %}escapes segment values in the legend and ARIA attributes;RichTextEditor/{% rich_text_editor %}cleansvalueto the Markdown component's tag and attribute allowlist (values marked safe pass through unchanged), so stored rich text keeps its formatting while scripts, event-handler attributes and non-http(s)/mailto URLs are removed (djust.components.utils.rich_html). The Rust engine passes list arguments to component handlers as data, so markup marked safe inside items is escaped there too; render such items with the component class or the Django tag. TheMasonryGridandDashboardGriddocstrings and the gallery examples now show text content. Tests inpython/djust/tests/test_rust_handler_item_escaping.py. -
Built-in components HTML-escape the values they render (1.0 backport of the 1.1.4 component escaping). Component classes,
{% ... %}component tags, their Rust-engine handlers and the Rust component renderers now pass interpolated values throughconditional_escape, in all CSS-framework variants; the relation selects (ForeignKeySelect,ManyToManySelect) escape model-derived labels. Strings passed to component slots and attributes are escaped unless marked safe: HTML passed to a content slot must be marked safe (mark_safe, or another component's rendered output), or it is shown as text. This affects modal, card, sheet, popover, tabs and accordion bodies, headers and footers, icons, and similar slots.href/srcvalues usingjavascript:,vbscript:or non-imagedata:render as#. The rich-text editor is the exception: its value is cleaned to an HTML allowlist (formatting kept, scripts and event attributes removed) instead of escaped. This is the same behaviour change 1.1.4 made. -
Theming URLs use a shared script-scheme denylist (backported from #3400, #3407, #3420). Data-derived URLs in navigation, sidebar sections, breadcrumbs, pagination, avatars and auth/error/empty-state pages are neutralised to
"#"forjavascript:,vbscript:anddata:schemes, including whitespace and control-character obfuscation. Avatar image sources retaindata:image/*; links do not. Other schemes, includingsms:,geo:and app deep links, remain unchanged. Developer-supplied buttonhref=andhref/src/action/formactionattributes passed to button, input, select and progress tags raiseValueError. Dictionary and object items in iterable collections are checked without mutating caller-owned items; pagination checks every formatted page, first, last, previous and next URL. Regression tests:python/djust/tests/test_theming_url_scheme_guard.py. -
@rate_limitnow applies on the page-POST fallback. The per-handler@rate_limitwas checked on WebSocket/SSE and on the HTTP API, but the page-POST fallback (mixins/request.post), which also runs an@event_handlerfrom the request body, did not consult it, so calls through it did not use the shared per-caller budget that F27 documents as "one budget per handler regardless of transport". The fallback now checks the limit at the same point as the WebSocket path (after the decorator and permission checks, before the handler runs) and answers429withcode: "rate_limited"in its existing JSON error shape. Both HTTP transports share one implementation,djust.rate_limit.http_handler_rate_check, and one caller-key helper,caller_key_for_request(still behindresolve_client_ip, soDJUST_TRUSTED_PROXY_COUNTstill applies); the HTTP API's behaviour is unchanged. Tests:python/djust/tests/test_ratelimit_post_fallback.py. -
One
@rate_limitbudget per handler function, whatever name reaches it. The shared store keyed buckets on the caller and the name the route used (the client-suppliedevent_nameon WebSocket/SSE, the function name on the HTTP API), so a method reachable under two names (an alias attribute, or a second name bound inmount) had a separate budget per name. The bucket is now keyed on the function that actually runs (__func__of a bound method), accepted only when that function is declared on the owning class; only the exact builtinstaticmethod/classmethodwrappers are unwrapped, and membership is compared withis. A handler whose identity cannot be determined this way (a property or other descriptor, afunctools.partial, a callable instance, a function from an unrelated class) is refused with a named error rather than given a budget of its own, and two handlers that resolve to one key with different limit settings are refused rather than sharing the first one's settings. Functions made by one factory keep separate budgets. Tests:python/djust/tests/test_ratelimit_post_fallback.py. -
Components check the CSS values they write into
styleattributes. A colour, length, ratio or number that a component takes from its caller and writes into an inlinestyleattribute is now checked against the shapes the component documents, and anything else is replaced by the component's default. Colours accept#rgb/#rgba/#rrggbb/#rrggbbaa, a CSS named colour (withtransparentandcurrentcolor),rgb()/rgba()/hsl()/hsla()with plain numeric arguments, andvar(--name)references; lengths accept numbers with a unit, the usual keywords,var(), andcalc()/min()/max()/clamp(). Valid values render exactly as before. CoversCursorsOverlayandCollabSelection(the user colour),CalendarViewevent colours,Metersegment colours,ColorPicker, kanban column colours, the data table colour scale, and the heights, widths, gaps, offsets and ratios ofMapPicker,RichTextEditor,ScrollArea,StickyHeader,DataGrid,DataTable,ResizablePanel,SortableGrid,DashboardGrid,AspectRatio,ResponsiveImage,SplitPane, the progress components and{% theme_skeleton %}, on every render path (component class, template tag and Rust template engine). The helpers aredjust.components.utils.safe_css_color,safe_css_length,safe_css_ratioandsafe_css_number. -
Live transports follow Django's
LoginRequiredMiddleware. Withdjango.contrib.auth.middleware.LoginRequiredMiddleware(Django 5.1+, or a subclass) inMIDDLEWARE, a view mounted over a live transport (WebSocket, SSE, API dispatch,mount_batch) now gets the answer the middleware gives for its page: an anonymous user is sent to the login page, where the middleware would send them (itslogin_urlandredirect_field_namefor that route), unless the view is public. A view is public when itsdispatchcarries Django'slogin_not_required(@method_decorator(login_not_required, name="dispatch"), public everywhere), or when the URLconf routes that view class throughlogin_not_required(MyView.as_view()), or when the request's own route serves the view and the installed middleware (subclass exemptions included) lets it through. A view the URLconf does not route, mounted beside the page view or hydrated as a lazy child over the WebSocket, needs thedispatchmarker to be mounted anonymously; naming a public page's URL does not make it public. When the middleware refuses such a view, only that view is refused (anerrorframe withcode: "login_required", or afailed[]entry in themount_batchreply) and the page stays where it is; the first view mounted on a socket is the page view, and its refusal sends the page to the login page. That is the middleware's answer: a view refused by its ownlogin_required = Truestill sends the page to the login page, and a view that relies only onpermission_required,check_permissions()or Django's auth mixins gets the middleware's answer first, so beside the page view it is refused on its own. On SSE and the API the request is the endpoint's, so a view served there anonymously needs thedispatchmarker or alogin_not_requiredroute of its own (and the endpoint itself must be exempt from the middleware for the request to arrive). Views rendered inside a page ({% live_render %}children) take their page's answer, as Django never checks what a page includes, and a page's own HTTP GET and POST are left to the middleware itself. djust's ownlogin_required = Falseis not an exemption, as it is not for the middleware. Without the middleware, or on Django < 5.1, nothing changes. -
A refused WebSocket mount no longer joins the view's channel groups.
LiveViewConsumer.handle_mountjoined the view's server-push group, its presence group and its db_notify groups, and started its tick, before the pre-mount auth checks ran. A view refused inside amount_batchleaves the socket open, so the connection kept receiving that view's presence broadcasts. The groups are now joined only once auth and theon_mounthooks admit the view, the tick starts once the object-permission check has passed, and a mount refused by that check leaves the groups and drops the presence itsmount()tracked.presence_eventandclient_push_eventare dropped when the connection has no mounted view, asserver_pushanddb_notifyalready were. -
A live view whose
mount()raises is no longer left mounted. An exception frommount()(or the state restore,handle_params()or the first render) used to answer with an error frame and keep the view as the WebSocket connection's mounted view, so later events still ran its handlers, and it stayed in its channel groups with its presence tracked. Such a mount now ends like a refused one: aPermissionDeniedis answered as the authorization check answers it (Permission denied; the page view's socket closes with 4403), any other exception keeps its error frame, and the view is released, its groups are left, its presence is untracked and its tick stops. A view mounted beside the page view (dj-lazy, amount_batchentry) goes alone and the page view stays. A view refused by the pre-mount permission check or by its object permission is cleared from the connection as well. Over SSE a failed mount also untracks its presence. The HTTP fallback and the HTTP API already refused such a request. -
Theme state read from cookies keeps only registered pack names and well-formed layout names, and
{% theme_css_link %}encodes its URL (1.0 backport of the 1.1.4 fix).ThemeManager.get_state()now keeps adjust_theme_packvalue only when that pack is registered; any other name falls back to the session pack, then the configured default, then no pack. Layout names must be simple identifiers ([A-Za-z0-9_-], up to 64 characters), otherwise the base layout is used.{% theme_css_link %}(and its Rust-engine handler) builds the query string withurlencodeand returns an HTML-escaped URL, so a cookie value can no longer leave thehrefattribute it is written into. -
The djust admin applies the
DjustModelAdminpermission hooks on the server (1.0 backport of the 1.1.4 fix). The change list needs view or change permission; the add page and its save need add permission; the change page needs view or change permission and its save needs change permission; the delete page,confirm_deleteand the built-indelete_selectedaction need delete permission. The checks run on mount and inside the save and delete handlers, so they hold on every transport. As in Django'sModelAdmin, the default hooks now checkrequest.user.has_perm("<app_label>.<action>_<model_name>")instead of returningTrue, and the admin index lists only models the user holds some permission on. Behaviour change: staff users need the model's Django permissions (superusers have them all); override the hooks for other rules. -
The
/_djust/observability/endpoints serve only direct local requests that carry the project's observability token (1.0 backport of the 1.1.4 fix). BesidesDEBUGand a loopbackREMOTE_ADDR, a request must carry none of the headers a reverse proxy adds (X-Forwarded-For,Forwarded,X-Real-IP,X-Forwarded-Host,X-Forwarded-Proto) and must send theX-Djust-Observability-Tokenheader: an HMAC ofSECRET_KEY, orDJUST_OBSERVABILITY_TOKENfrom the environment when set. A reverse proxy on the same host connects from loopback, so the peer address alone did not show where a request came from.LocalhostOnlyObservabilityMiddlewareapplies the same network check. The djust MCP server sends the token automatically when started withmanage.py djust_mcp;manage.py djust_observability_tokenprints it for other tools. -
A reused sticky
{% live_render %}child is re-authorized on every parent render (1.0 backport of the 1.1.4 fix). When a parent re-render reused a livesticky=Truechild, the view-level and object-level checks that a fresh embed runs were skipped, so a child kept rendering after the user lost access to it. The reuse path now runscheck_view_authandenforce_object_permissionagainst the current request; on denial the child is dropped from the parent (its cleanup hooks run) and the embed is refused, exactly as a fresh mount would be. Sticky children carried across alive_redirectare also checked at object level before they are kept. -
Resumable uploads record and check their owning session (1.0 backport of the 1.1.4 fix).
upload_registernow stores the connection's Django session key on the upload, and aResumableUploadWriterrecords it as the owner.upload_resume(WebSocket) and the HTTP upload-status view answernot_foundunless the requesting session is that owner; an upload with no recorded owner session cannot be resumed. Previously the owner was never recorded, so the ownership check never applied and any connection that knew an upload id could read its progress. -
Six template-layer escaping defects closed (1.0 backport of the 1.1.1 fixes).
{{ value|unordered_list }}and{{ value|safeseq }}on a string now escape it;{% render_slot %}escapes a plain context value it would otherwise echo (a slot entry's pre-renderedcontentstays as rendered); amark_safegrant on aRustLiveViewkey is revoked whenupdate_statereplaces that key's value, so it no longer outlives the value for the life of the connection;|escapeescapes instead of being a no-op, so{{ value|escape|safe }}is inert;linenumbers,linebreaksandlinebreaksbrescape each line before adding their own markup, so{{ value|linebreaks|safe }}is inert and the bare{{ value|linebreaks }}now renders real<p>/<br>tags around escaped text. These four filters report their output as already escaped only at the last position in the chain, so{{ value|escape|F }}for a plain filterFdouble-escapes (safe direction, same as 1.1.1). Tests inpython/djust/tests/test_security_1_1_1_backports.py. -
A template binding replaces the safety grant of the name it binds instead of inheriting it (1.0 backport of the 1.1.2 fix).
{% with %},{% for %}(including tuple unpacking),{% include ... with %}and custom assign tags ({% tag ... as x %}) bound a new value to a name that the context had marked safe (and the names beneath it, such asx.field) without dropping that mark, so the new, unescaped value was emitted raw. A bind now revokes the grants on the bound name and its descendants, scoped to the bind's own context; grants a value legitimately carries (the{% for %}loop mapping, an untouched outer name) are kept. Tests inpython/djust/tests/test_security_1_1_2_binding_grant.py. -
Custom template filters registered with
is_safe=Truekeep their output escaped unless their input was already safe (1.0 backport of the 1.1.4 fix). The Rust renderer treatedis_safe=Trueon a custom filter as "this filter's output is safe", so a filter returning a plainstrbuilt from user input rendered it unescaped on every djust render path. It now follows Django's rule (is_safe and isinstance(value, SafeData)): the flag keeps a safe input safe through the filter and never makes plain input safe; a filter that returns aSafeString(ormark_safeoutput) is still emitted as returned. Tests inpython/tests/test_custom_filter_is_safe_requires_safe_input.py. -
React component props are serialised as encoded JSON (1.0 backport of the 1.1.5 fix). Props were written into the single-quoted
data-react-propsattribute with only double quotes escaped, so a prop value containing'ended the attribute and the rest of the value was parsed as markup. The Rust renderer and the Python post-processing path now serialise props withjson.dumps-equivalent encoding and fully HTML-escape the attribute value; the client reads the attribute throughdataset, which decodes the entities, and parses it as JSON. Tests inpython/djust/tests/test_react_props_serialization.py. -
DataTable sorts, filters and groups only on declared columns (1.0 backport of the 1.1.4 / 1.2.0rc11 change).
on_table_sort,on_table_filter, group-by and expression filters now accept only keys listed intable_columns, checked again where the queryset is built. Sorting is on unless a column sets"sortable": False; filtering needs"filterable": True;table_default_sortis always accepted. Unknown column names are ignored, so the client can no longer filter or order a table's queryset on a field the table does not declare, including fields reached through relations. Tests inpython/tests/test_data_table_declared_columns.py. -
The djust admin change list filters and sorts only on declared fields.
ModelListView.apply_filteraccepts the field names in the ModelAdmin'slist_filter(get_list_filter()), andsort_byaccepts the sortablelist_displaycolumns and the fields named inordering(get_ordering()), as Django's admin does. The queryset applies the same check to the stored filters and ordering, so any other name (another lookup, a path through a relation, an expression) is ignored and logged at debug level. The filter list on the page now comes fromget_list_filter()as well. A custom change-list template that filters or sorts on other fields must declare them inlist_filter,list_displayorordering. The auth admin's social accounts list sorts only on its own columns. -
The djust admin loads rows through the ModelAdmin's
get_queryset(). ADjustModelAdminthat scopes rows inget_queryset(request)was honoured by the change list only: bulk actions ran on the selected primary keys throughModel.objects, and the change and delete views loaded their object withModel.objects.get(). As in Django's admin, an action now runs on the selected rows thatget_queryset(request)holds (other selected keys are ignored), and the change and delete views load their object with the newDjustModelAdmin.get_object(request, object_id), which looks it up in that queryset; a row it leaves out, or a key that names no row, is a 404. -
crossbeam-epoch0.9.18 → 0.9.20 (RUSTSEC-2026-0204, backported from #2125). An invalid pointer dereference in thefmt::Pointerimpl forAtomic/Shared. Transitive (throughrayon-core), lockfile-only, no manifest change.
Changed
- The VDOM bounds the nesting depth of the HTML it parses. Every walker over the VDOM (diff, patch serialization, serialization back to HTML) recurses once per nesting level, so a page whose markup nests thousands of elements deep needs more stack than the rendering thread has. The parser now keeps at most
MAX_VDOM_DEPTH(256) levels, counted from the LiveView root: an element at the limit keeps its tag and attributes, and everything below it is flattened to its text content, so nothing nests deeper and no text is lost (except below an SVG or MathML<style>or<script>at the limit, which keeps no text: its descendants' text is not its own). Pages are unaffected: the deepest of 92 rendered demo-project pages and 316 template files nests 14 levels, and parsing, diffing and serializing them gives byte-identical results before and after. The search for thedj-rootelement no longer walks the raw tree recursively either. The limit is exported asdjust._rust.MAX_VDOM_DEPTH. Regression cases inpython/tests/test_vdom_nesting_bound.pyandcrates/djust_vdom/tests/test_nesting_depth_bound.rs. - Terminal's ANSI conversion keeps a bounded number of styles open per line. Each open style is a nested
<span>until a reset or the end of the line, so a short line could nest as deep as it was long. The conversion (in theTerminalcomponent, the inline handler and the{% terminal %}tag) now keeps at most 32 styles open per line and opens styles from at most the first 16 parameters of a sequence (the client hook reads the same 16); a style past either cap is dropped, and a reset anywhere in a sequence closes every open style. Lines that stay under the caps, which is every line a program prints in practice, render byte for byte as before. Regression cases inpython/tests/test_terminal_ansi_span_cap.py. - Markdown output keeps at most 32 containers open at once.
djust.render_markdownand{% djust_markdown %}emit one<blockquote>per>and a<ul>/<li>pair per list level, so the depth of the output, and the time spent parsing and diffing it, grew with the nesting of the source. A block quote, list or list item that starts whileMAX_MD_NESTING(32) containers are already open is now rendered without its own tags; its content stays in place inside the innermost kept container, so no text is lost, every kept<li>stays balanced, and the cost of rendering stays proportional to the length of the source. Documents that nest 32 containers or fewer (ordinary documents: 914 distinct Markdown files from the docs of this repository and its sibling projects all render unchanged, and the deepest nests 6) render byte for byte as before. The bound is exported asdjust._rust.MAX_MD_NESTING. Regression cases inpython/tests/test_markdown_nesting_bound.pyand themarkdownunit tests ofdjust_templates.
Fixed
- The HTTP POST event transport now enforces the same authorization as every other transport.
post()applied only the event-name format check and the@event_handlerdecorator policy; the three authorization layers thatget()and the WebSocket/SSE event paths apply — view-levellogin_required/permission_required, handler-level@permission_required, and the ADR-017 object-level check — were not consulted, so an event could be dispatched to a caller the other transports would refuse. All three now run before dispatch, via the samecheck_view_auth/check_handler_permission/enforce_object_permissionentry points the other paths use. - A malformed request body no longer masks its own cause.
post()'sexceptblock referencedevent_nameandparams, which are assigned inside thetry, so a failure before their assignment (a body that is not valid JSON is the easy one) raisedUnboundLocalErrorfrom the error path itself — reporting a controlled failure as an unexplained 500 and never logging the real exception. Both names are now initialised before thetry. - Test Git fixtures no longer inherit repository execution variables from Git hooks (#3179). Fixture helpers isolate their environment, Git-spawning test modules strip
GIT_EXECUTION_VARS, and a root fixture covers library code that runs Git in-process. Static and behavioural guards prevent fixture identity and signing settings from leaking into the real repository's.git/config. - Text in an SVG or MathML
<style>/<script>is serialized as escaped text. The VDOM wrote the text of any element namedscriptorstyleverbatim, by tag name alone. The HTML serialization rules apply that only to HTML elements: inside<svg>or<math>these are foreign elements, whose text the parser decodes, so it has to be escaped again when written back (<stays<). The serializer now tracks the namespace while it writes (svgandmathswitch to foreign content;foreignObject,desc,title, MathML text elements and HTMLannotation-xmlswitch back), so re-parsing the HTML gives the same tree. HTML<style>and<script>output is byte-identical, and so is SVG CSS without<,>or&. HTML<script>and<style>content is written verbatim exactly as the HTML serialization algorithm writes it, including a script that holds</script>inside<!-- … -->(the legacydocument.writepattern). An inserted{% if %}block (the HTML of anInsertSubtreepatch) is written the way the page writes it, since the client now parses it in its parent's namespace. Therender_binary_difftext-only fast path now updates a text node only when the HTML's text runs and the tree's text nodes line up one for one, and never inside a raw-text element: it mapped changes by byte offset, so dropped whitespace or text outside the root sent a change to the neighbouring node, and it decoded<inside a<script>. The text fast paths (the in-place update,try_text_only_vdom_update,try_text_patches, and the fragment and text-region paths ofrender_with_diff) also require each text node to sit in the element the source has open around its text: text directly inside a<table>is moved before the table by the parser, so two equal texts could line up in the wrong order and a change was applied to the other node. Such updates, and any start tag the parser would move out of a table, now take the full parse. The text fast path also falls back to a full parse for every<style>or<script>that follows an<svg>or<math>start tag; it used to count</svg>/</math>to decide when foreign content closed, and the tree builder ignores some of those end tags (a stray</math>inside<svg>, an</svg>insideforeignObjectordesccontent), so a text change could still be sent to the wrong node. The serializer also starts from the real namespace of the root: the parser reports it, so adj-rooton an element inside<svg>or<math>(<svg><g dj-root>) writes its<style>/<script>text escaped, as does itsdj-update="ignore"cache, and the HTML of anInsertSubtreepatch is written for its parent's namespace; the text fast paths run only under an HTML root. Regression cases inpython/tests/test_vdom_foreign_raw_text.py,python/tests/test_vdom_html_raw_text_verbatim.pyand incrates/djust_vdom/src/lib.rs. - An inserted
{% if %}block inside<svg>or<math>keeps the SVG / MathML namespace. The client parsed the HTML of everyInsertSubtreepatch in an HTML<template>, so the elements of a conditional block inside<svg>or<math>came out as HTML elements, while the full render has them as SVG or MathML elements: an<iframe>,<a>,<form>,<details>or<video>written there is inert in the full render but was a working HTML element after the patch. The client now parses that HTML as children of the element it is inserted into: inside an<svg>or<math>wrapper for an SVG or MathML parent (a MathMLmi,mo,mn,ms,mtextorannotation-xmlparent is reproduced as well), and as HTML, as before, for an HTML parent and for the HTML integration points (foreignObject,desc,title, andannotation-xmlwith an HTMLencoding). The server writes that HTML for that parse. Regression tests intests/js/insert_subtree_namespace.test.js,python/tests/test_vdom_foreign_raw_text.pyandcrates/djust_vdom/src/lib.rs;tests/playwright/test_insert_subtree_namespace.pycompares real patches with a full render in Chromium. - A node inserted inside
<svg>or<math>by anInsertChildpatch keeps the SVG / MathML namespace. The client chose an inserted element's namespace from its name alone: an allow-listed HTML name (iframe,a,form,details,video) inside<svg>or<math>became a working HTML element where the full render has an inert SVG or MathML element, and MathML names became a<span>. The client now derives the namespace from the element the node is inserted into, by the same rules a page parse follows (foreignObject,desc,title, the MathML text elements and HTMLannotation-xmlhold HTML), and creates allow-listed names, MathML element names included, in that namespace. Names outside the allow lists still become a<span>, and an element under an HTML parent is created as before. Regression tests intests/js/insert_subtree_namespace.test.js;tests/playwright/test_insert_subtree_namespace.pycoversInsertChildin Chromium too. - A dj-root on an SVG or MathML element keeps its children's namespace through
html_update,html_recoveryand the morph of a prerendered page on mount. These three paths parsed the root's inner HTML in an HTML context (DOMParser, orinnerHTMLon a<div>), so every child of a dj-root placed on an SVG or MathML element came out as an HTML element and replaced the SVG / MathML element of the full render: an<iframe>,<a>,<form>,<details>or<video>written there was a working HTML element after the update, where the full render has an inert SVG or MathML element.html_updateandhtml_recoverynow parse the frame as the root's children in the root's namespace, inertly (_parseRootFrameHtml), and the mount morph parses it in a detached element of the root's own namespace and name (_parseRootHtmlLive). A dj-root on an HTML element parses exactly as before. Regression tests intests/js/full_html_frame_namespace.test.js. - The mount morph no longer runs a MathML or SVG
<script>. After the morph of a prerendered page, the client re-creates each classic<script>in the dj-root as a new HTML<script>so that inline page scripts run (#1848). It also matched<script>elements inside<math>and<svg>and re-created them as HTML scripts, so a MathML<script>, which never runs in a full render, ran after the morph._runInsertedScriptsnow re-creates only HTML<script>elements. Regression test intests/js/mount-morph-script-exec-1848.test.js. - The children of a placeholder for an SVG or MathML element keep their namespace. An
InsertChildpatch turns an SVG or MathML element outside the client's allow lists (filter,fe*,animate*,set,mstack, an unknown name) into a<span>, and then created its children inside that HTML<span>: an<iframe>,<a>,<form>,<details>or<video>there became a working HTML element, where the full render has an inert SVG or MathML element. The children are now created in the namespace of the element the<span>stands in for. The placeholder itself stays a<span>. Regression tests intests/js/insert_subtree_namespace.test.js.
What's Changed
- fix(tests): git fixtures must not write into the real .git/config (1.0 backport of #3179) by @johnrtipton in #3445
- fix(theming): neutralise script-bearing data-derived URLs in theming tags (1.0 backport) by @johnrtipton in #3446
- chore(release): 1.0.10 by @johnrtipton in #3460
Full Changelog: v1.0.9...v1.0.10