pyweber 1.5.0.dev2__tar.gz → 1.5.0.dev3__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {pyweber-1.5.0.dev2/pyweber.egg-info → pyweber-1.5.0.dev3}/PKG-INFO +1 -1
- {pyweber-1.5.0.dev2 → pyweber-1.5.0.dev3}/pyproject.toml +1 -1
- {pyweber-1.5.0.dev2 → pyweber-1.5.0.dev3}/pyweber/core/template.py +12 -1
- {pyweber-1.5.0.dev2 → pyweber-1.5.0.dev3}/pyweber/models/response.py +16 -13
- {pyweber-1.5.0.dev2 → pyweber-1.5.0.dev3}/pyweber/models/ws_message.py +15 -3
- {pyweber-1.5.0.dev2 → pyweber-1.5.0.dev3}/pyweber/pyweber/pyweber.py +8 -1
- {pyweber-1.5.0.dev2 → pyweber-1.5.0.dev3}/pyweber/static/config.toml +1 -0
- {pyweber-1.5.0.dev2 → pyweber-1.5.0.dev3}/pyweber/static/js.js +26 -5
- {pyweber-1.5.0.dev2 → pyweber-1.5.0.dev3}/pyweber/utils/security.py +34 -0
- {pyweber-1.5.0.dev2 → pyweber-1.5.0.dev3/pyweber.egg-info}/PKG-INFO +1 -1
- {pyweber-1.5.0.dev2 → pyweber-1.5.0.dev3}/LICENSE +0 -0
- {pyweber-1.5.0.dev2 → pyweber-1.5.0.dev3}/MANIFEST.in +0 -0
- {pyweber-1.5.0.dev2 → pyweber-1.5.0.dev3}/NOTICE +0 -0
- {pyweber-1.5.0.dev2 → pyweber-1.5.0.dev3}/README.md +0 -0
- {pyweber-1.5.0.dev2 → pyweber-1.5.0.dev3}/pyweber/__init__.py +0 -0
- {pyweber-1.5.0.dev2 → pyweber-1.5.0.dev3}/pyweber/admin/index.html +0 -0
- {pyweber-1.5.0.dev2 → pyweber-1.5.0.dev3}/pyweber/admin/src/script.js +0 -0
- {pyweber-1.5.0.dev2 → pyweber-1.5.0.dev3}/pyweber/admin/src/style.css +0 -0
- {pyweber-1.5.0.dev2 → pyweber-1.5.0.dev3}/pyweber/cli/__init__.py +0 -0
- {pyweber-1.5.0.dev2 → pyweber-1.5.0.dev3}/pyweber/cli/commands.py +0 -0
- {pyweber-1.5.0.dev2 → pyweber-1.5.0.dev3}/pyweber/components/__init__.py +0 -0
- {pyweber-1.5.0.dev2 → pyweber-1.5.0.dev3}/pyweber/components/form.py +0 -0
- {pyweber-1.5.0.dev2 → pyweber-1.5.0.dev3}/pyweber/components/general.py +0 -0
- {pyweber-1.5.0.dev2 → pyweber-1.5.0.dev3}/pyweber/components/input.py +0 -0
- {pyweber-1.5.0.dev2 → pyweber-1.5.0.dev3}/pyweber/config/__init__.py +0 -0
- {pyweber-1.5.0.dev2 → pyweber-1.5.0.dev3}/pyweber/config/config.py +0 -0
- {pyweber-1.5.0.dev2 → pyweber-1.5.0.dev3}/pyweber/connection/__init__.py +0 -0
- {pyweber-1.5.0.dev2 → pyweber-1.5.0.dev3}/pyweber/connection/http.py +0 -0
- {pyweber-1.5.0.dev2 → pyweber-1.5.0.dev3}/pyweber/connection/reload.py +0 -0
- {pyweber-1.5.0.dev2 → pyweber-1.5.0.dev3}/pyweber/connection/selector.py +0 -0
- {pyweber-1.5.0.dev2 → pyweber-1.5.0.dev3}/pyweber/connection/session.py +0 -0
- {pyweber-1.5.0.dev2 → pyweber-1.5.0.dev3}/pyweber/connection/websocket.py +0 -0
- {pyweber-1.5.0.dev2 → pyweber-1.5.0.dev3}/pyweber/core/__init__.py +0 -0
- {pyweber-1.5.0.dev2 → pyweber-1.5.0.dev3}/pyweber/core/element.py +0 -0
- {pyweber-1.5.0.dev2 → pyweber-1.5.0.dev3}/pyweber/core/events.py +0 -0
- {pyweber-1.5.0.dev2 → pyweber-1.5.0.dev3}/pyweber/core/html_parser.py +0 -0
- {pyweber-1.5.0.dev2 → pyweber-1.5.0.dev3}/pyweber/core/window.py +0 -0
- {pyweber-1.5.0.dev2 → pyweber-1.5.0.dev3}/pyweber/models/__init__.py +0 -0
- {pyweber-1.5.0.dev2 → pyweber-1.5.0.dev3}/pyweber/models/context.py +0 -0
- {pyweber-1.5.0.dev2 → pyweber-1.5.0.dev3}/pyweber/models/cookies.py +0 -0
- {pyweber-1.5.0.dev2 → pyweber-1.5.0.dev3}/pyweber/models/create_app.py +0 -0
- {pyweber-1.5.0.dev2 → pyweber-1.5.0.dev3}/pyweber/models/element.py +0 -0
- {pyweber-1.5.0.dev2 → pyweber-1.5.0.dev3}/pyweber/models/error_pages.py +0 -0
- {pyweber-1.5.0.dev2 → pyweber-1.5.0.dev3}/pyweber/models/field.py +0 -0
- {pyweber-1.5.0.dev2 → pyweber-1.5.0.dev3}/pyweber/models/field_storage.py +0 -0
- {pyweber-1.5.0.dev2 → pyweber-1.5.0.dev3}/pyweber/models/file.py +0 -0
- {pyweber-1.5.0.dev2 → pyweber-1.5.0.dev3}/pyweber/models/file_stream.py +0 -0
- {pyweber-1.5.0.dev2 → pyweber-1.5.0.dev3}/pyweber/models/handoff.py +0 -0
- {pyweber-1.5.0.dev2 → pyweber-1.5.0.dev3}/pyweber/models/headers.py +0 -0
- {pyweber-1.5.0.dev2 → pyweber-1.5.0.dev3}/pyweber/models/middleware.py +0 -0
- {pyweber-1.5.0.dev2 → pyweber-1.5.0.dev3}/pyweber/models/openapi.py +0 -0
- {pyweber-1.5.0.dev2 → pyweber-1.5.0.dev3}/pyweber/models/rate_limit.py +0 -0
- {pyweber-1.5.0.dev2 → pyweber-1.5.0.dev3}/pyweber/models/request.py +0 -0
- {pyweber-1.5.0.dev2 → pyweber-1.5.0.dev3}/pyweber/models/routes.py +0 -0
- {pyweber-1.5.0.dev2 → pyweber-1.5.0.dev3}/pyweber/models/run.py +0 -0
- {pyweber-1.5.0.dev2 → pyweber-1.5.0.dev3}/pyweber/models/security.py +0 -0
- {pyweber-1.5.0.dev2 → pyweber-1.5.0.dev3}/pyweber/models/stream_stats.py +0 -0
- {pyweber-1.5.0.dev2 → pyweber-1.5.0.dev3}/pyweber/models/strem_stats.py +0 -0
- {pyweber-1.5.0.dev2 → pyweber-1.5.0.dev3}/pyweber/models/task_manager.py +0 -0
- {pyweber-1.5.0.dev2 → pyweber-1.5.0.dev3}/pyweber/models/template_diff.py +0 -0
- {pyweber-1.5.0.dev2 → pyweber-1.5.0.dev3}/pyweber/pyweber/__init__.py +0 -0
- {pyweber-1.5.0.dev2 → pyweber-1.5.0.dev3}/pyweber/static/.gitignore +0 -0
- {pyweber-1.5.0.dev2 → pyweber-1.5.0.dev3}/pyweber/static/css.css +0 -0
- {pyweber-1.5.0.dev2 → pyweber-1.5.0.dev3}/pyweber/static/docs.html +0 -0
- {pyweber-1.5.0.dev2 → pyweber-1.5.0.dev3}/pyweber/static/favicon/android-chrome-192x192.png +0 -0
- {pyweber-1.5.0.dev2 → pyweber-1.5.0.dev3}/pyweber/static/favicon/android-chrome-512x512.png +0 -0
- {pyweber-1.5.0.dev2 → pyweber-1.5.0.dev3}/pyweber/static/favicon/apple-touch-icon.png +0 -0
- {pyweber-1.5.0.dev2 → pyweber-1.5.0.dev3}/pyweber/static/favicon/favicon-16x16.png +0 -0
- {pyweber-1.5.0.dev2 → pyweber-1.5.0.dev3}/pyweber/static/favicon/favicon-32x32.png +0 -0
- {pyweber-1.5.0.dev2 → pyweber-1.5.0.dev3}/pyweber/static/favicon/favicon.ico +0 -0
- {pyweber-1.5.0.dev2 → pyweber-1.5.0.dev3}/pyweber/static/favicon/pyweber.png +0 -0
- {pyweber-1.5.0.dev2 → pyweber-1.5.0.dev3}/pyweber/static/favicon/site.webmanifest +0 -0
- {pyweber-1.5.0.dev2 → pyweber-1.5.0.dev3}/pyweber/static/handlers.js +0 -0
- {pyweber-1.5.0.dev2 → pyweber-1.5.0.dev3}/pyweber/static/html.html +0 -0
- {pyweber-1.5.0.dev2 → pyweber-1.5.0.dev3}/pyweber/static/html401.html +0 -0
- {pyweber-1.5.0.dev2 → pyweber-1.5.0.dev3}/pyweber/static/html404.html +0 -0
- {pyweber-1.5.0.dev2 → pyweber-1.5.0.dev3}/pyweber/static/html500.html +0 -0
- {pyweber-1.5.0.dev2 → pyweber-1.5.0.dev3}/pyweber/static/loading.html +0 -0
- {pyweber-1.5.0.dev2 → pyweber-1.5.0.dev3}/pyweber/static/main.py +0 -0
- {pyweber-1.5.0.dev2 → pyweber-1.5.0.dev3}/pyweber/static/pyweber.css +0 -0
- {pyweber-1.5.0.dev2 → pyweber-1.5.0.dev3}/pyweber/static/update.py +0 -0
- {pyweber-1.5.0.dev2 → pyweber-1.5.0.dev3}/pyweber/testing/__init__.py +0 -0
- {pyweber-1.5.0.dev2 → pyweber-1.5.0.dev3}/pyweber/testing/client.py +0 -0
- {pyweber-1.5.0.dev2 → pyweber-1.5.0.dev3}/pyweber/utils/__init__.py +0 -0
- {pyweber-1.5.0.dev2 → pyweber-1.5.0.dev3}/pyweber/utils/async_utils.py +0 -0
- {pyweber-1.5.0.dev2 → pyweber-1.5.0.dev3}/pyweber/utils/deprecation.py +0 -0
- {pyweber-1.5.0.dev2 → pyweber-1.5.0.dev3}/pyweber/utils/exceptions.py +0 -0
- {pyweber-1.5.0.dev2 → pyweber-1.5.0.dev3}/pyweber/utils/icons.py +0 -0
- {pyweber-1.5.0.dev2 → pyweber-1.5.0.dev3}/pyweber/utils/loads.py +0 -0
- {pyweber-1.5.0.dev2 → pyweber-1.5.0.dev3}/pyweber/utils/mime.py +0 -0
- {pyweber-1.5.0.dev2 → pyweber-1.5.0.dev3}/pyweber/utils/types.py +0 -0
- {pyweber-1.5.0.dev2 → pyweber-1.5.0.dev3}/pyweber/utils/utils.py +0 -0
- {pyweber-1.5.0.dev2 → pyweber-1.5.0.dev3}/pyweber.egg-info/SOURCES.txt +0 -0
- {pyweber-1.5.0.dev2 → pyweber-1.5.0.dev3}/pyweber.egg-info/dependency_links.txt +0 -0
- {pyweber-1.5.0.dev2 → pyweber-1.5.0.dev3}/pyweber.egg-info/entry_points.txt +0 -0
- {pyweber-1.5.0.dev2 → pyweber-1.5.0.dev3}/pyweber.egg-info/requires.txt +0 -0
- {pyweber-1.5.0.dev2 → pyweber-1.5.0.dev3}/pyweber.egg-info/top_level.txt +0 -0
- {pyweber-1.5.0.dev2 → pyweber-1.5.0.dev3}/setup.cfg +0 -0
- {pyweber-1.5.0.dev2 → pyweber-1.5.0.dev3}/tests/test_config.py +0 -0
- {pyweber-1.5.0.dev2 → pyweber-1.5.0.dev3}/tests/test_context.py +0 -0
- {pyweber-1.5.0.dev2 → pyweber-1.5.0.dev3}/tests/test_cookies.py +0 -0
- {pyweber-1.5.0.dev2 → pyweber-1.5.0.dev3}/tests/test_public_api.py +0 -0
- {pyweber-1.5.0.dev2 → pyweber-1.5.0.dev3}/tests/test_response.py +0 -0
- {pyweber-1.5.0.dev2 → pyweber-1.5.0.dev3}/tests/test_template_diff.py +0 -0
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "pyweber"
|
|
7
|
-
version = "1.5.0.
|
|
7
|
+
version = "1.5.0.dev3"
|
|
8
8
|
description = "A lightweight Python framework for building and managing web applications."
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
authors = [{ name = "DevPythonMZ", email = "pypi.dev@gmail.com" }]
|
|
@@ -15,6 +15,11 @@ class Template:
|
|
|
15
15
|
self.title = title
|
|
16
16
|
self.__root = self.parse_html()
|
|
17
17
|
|
|
18
|
+
@property
|
|
19
|
+
def include_uuid(self) -> bool:
|
|
20
|
+
"""When False, HTML is static (no uuid attrs) and the reactive WS script is not injected."""
|
|
21
|
+
return self.__include_uuid
|
|
22
|
+
|
|
18
23
|
@property
|
|
19
24
|
def template(self):
|
|
20
25
|
return self.__template
|
|
@@ -135,6 +140,7 @@ class Template:
|
|
|
135
140
|
return element
|
|
136
141
|
|
|
137
142
|
def __create_default_element(self, *args, **kwargs):
|
|
143
|
+
kwargs.setdefault('include_uuid', self.__include_uuid)
|
|
138
144
|
return Element(*args, **kwargs)
|
|
139
145
|
|
|
140
146
|
@staticmethod
|
|
@@ -204,9 +210,13 @@ class Template:
|
|
|
204
210
|
break
|
|
205
211
|
|
|
206
212
|
if not has_websocket_script:
|
|
213
|
+
# Reactivity requires stable uuid attrs in HTML. Static pages
|
|
214
|
+
# (include_uuid=False) must not load the WS client — it stamps
|
|
215
|
+
# random uuids and can rewrite documentElement (breaking CSS).
|
|
207
216
|
disable_ws = os.environ.get('PYWEBER_DISABLE_WS', False)
|
|
217
|
+
ws_disabled = disable_ws in [True, 'True', 'true', '1', 1]
|
|
208
218
|
|
|
209
|
-
if
|
|
219
|
+
if self.__include_uuid and not ws_disabled:
|
|
210
220
|
head.childs.extend(
|
|
211
221
|
[
|
|
212
222
|
self.__create_default_element(
|
|
@@ -269,6 +279,7 @@ class Template:
|
|
|
269
279
|
template=self.template,
|
|
270
280
|
status_code=self.status_code,
|
|
271
281
|
title=self.title,
|
|
282
|
+
include_uuid=self.__include_uuid,
|
|
272
283
|
**self.kwargs
|
|
273
284
|
)
|
|
274
285
|
tpl.data = self.data
|
|
@@ -7,19 +7,16 @@ from typing import Any
|
|
|
7
7
|
from pyweber.utils.types import ContentTypes, HTTPStatusCode
|
|
8
8
|
from pyweber.models.request import Request
|
|
9
9
|
from pyweber.utils.utils import PrintLine, Colors
|
|
10
|
-
from pyweber.utils.security import
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
"style-src 'self' 'unsafe-inline'; "
|
|
16
|
-
"img-src 'self' data:; "
|
|
17
|
-
"connect-src 'self' ws: wss:; "
|
|
18
|
-
"frame-ancestors 'none'; "
|
|
19
|
-
"base-uri 'self'; "
|
|
20
|
-
"form-action 'self'"
|
|
10
|
+
from pyweber.utils.security import (
|
|
11
|
+
DEFAULT_CSP,
|
|
12
|
+
get_allowed_origins,
|
|
13
|
+
https_enabled,
|
|
14
|
+
resolve_csp,
|
|
21
15
|
)
|
|
22
16
|
|
|
17
|
+
# Re-export for callers that imported DEFAULT_CSP from response
|
|
18
|
+
__all_csp__ = (DEFAULT_CSP, resolve_csp)
|
|
19
|
+
|
|
23
20
|
# Internal bookkeeping headers — stripped on ASGI wire format
|
|
24
21
|
INTERNAL_RESPONSE_HEADERS = frozenset({
|
|
25
22
|
'method',
|
|
@@ -121,11 +118,14 @@ class Response:
|
|
|
121
118
|
"X-Content-Type-Options": "nosniff",
|
|
122
119
|
"X-Frame-Options": "DENY",
|
|
123
120
|
"Referrer-Policy": "strict-origin-when-cross-origin",
|
|
124
|
-
"Content-Security-Policy": DEFAULT_CSP,
|
|
125
121
|
"X-Forwarded-Proto": "https" if https_enabled() else "http",
|
|
126
122
|
"X-Forwarded-Host": getattr(request, 'host', None) or 'localhost',
|
|
127
123
|
}
|
|
128
124
|
|
|
125
|
+
csp = resolve_csp()
|
|
126
|
+
if csp:
|
|
127
|
+
self.__headers["Content-Security-Policy"] = csp
|
|
128
|
+
|
|
129
129
|
if https_enabled():
|
|
130
130
|
self.__headers["Strict-Transport-Security"] = "max-age=31536000; includeSubDomains"
|
|
131
131
|
|
|
@@ -133,7 +133,10 @@ class Response:
|
|
|
133
133
|
|
|
134
134
|
if headers:
|
|
135
135
|
for key, value in headers.items():
|
|
136
|
-
|
|
136
|
+
if value is None:
|
|
137
|
+
self.__headers.pop(key, None)
|
|
138
|
+
else:
|
|
139
|
+
self.__headers[key] = value
|
|
137
140
|
|
|
138
141
|
self.http_status_code = HTTPStatusCode.search_by_code(status)
|
|
139
142
|
self.__check_status_code()
|
|
@@ -41,6 +41,7 @@ class wsMessage:
|
|
|
41
41
|
|
|
42
42
|
async def get_template(self):
|
|
43
43
|
self.__app: 'Pyweber' = self.__app
|
|
44
|
+
used_handoff = False
|
|
44
45
|
if self.session_id in sessions.all_sessions:
|
|
45
46
|
session_template = sessions.get_session(session_id=self.session_id).template
|
|
46
47
|
else:
|
|
@@ -48,12 +49,23 @@ class wsMessage:
|
|
|
48
49
|
token=self.get_value(key='handoffToken'),
|
|
49
50
|
route=self.route or '',
|
|
50
51
|
)
|
|
52
|
+
if session_template is not None:
|
|
53
|
+
used_handoff = True
|
|
51
54
|
if session_template is None:
|
|
52
55
|
session_template = await self.__app.clone_template(route=self.route)
|
|
53
56
|
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
+
client_html = self.get_value(key='template')
|
|
58
|
+
if client_html:
|
|
59
|
+
# With include_uuid=False (or handoff without uuid attrs in HTML),
|
|
60
|
+
# never parse client outerHTML over the server template — client
|
|
61
|
+
# may have stamped random uuids that poison the diff contract.
|
|
62
|
+
keep_server = (
|
|
63
|
+
not getattr(session_template, 'include_uuid', True)
|
|
64
|
+
or (used_handoff and 'uuid=' not in client_html.lower())
|
|
65
|
+
)
|
|
66
|
+
if not keep_server:
|
|
67
|
+
session_template.root = session_template.parse_html(html=client_html)
|
|
68
|
+
self.insert_values(element=session_template.root)
|
|
57
69
|
|
|
58
70
|
return session_template
|
|
59
71
|
|
|
@@ -591,7 +591,14 @@ class Pyweber(
|
|
|
591
591
|
return False
|
|
592
592
|
|
|
593
593
|
template = template_result.template
|
|
594
|
-
|
|
594
|
+
if isinstance(template, (dict, list, set)):
|
|
595
|
+
return False
|
|
596
|
+
# No stable uuid contract → no reactive session handoff
|
|
597
|
+
if isinstance(template, Template) and not template.include_uuid:
|
|
598
|
+
return False
|
|
599
|
+
if isinstance(template, Element) and not getattr(template, 'include_uuid', True):
|
|
600
|
+
return False
|
|
601
|
+
return True
|
|
595
602
|
|
|
596
603
|
def _ensure_template_object(
|
|
597
604
|
self,
|
|
@@ -41,7 +41,9 @@ function connectWebSocket() {
|
|
|
41
41
|
socket.onopen = async function () {
|
|
42
42
|
socketReady = true;
|
|
43
43
|
reconnectAttempts = 0;
|
|
44
|
-
|
|
44
|
+
// Só envia outerHTML no handshake se existir contrato uuid estável
|
|
45
|
+
const includeTemplate = pageHasStableUuids();
|
|
46
|
+
sendToServer(await getEventData({ includeTemplate }));
|
|
45
47
|
};
|
|
46
48
|
|
|
47
49
|
socket.onerror = function (error) {
|
|
@@ -250,20 +252,34 @@ async function send_file_chunk(file_id, status, data) {
|
|
|
250
252
|
}
|
|
251
253
|
|
|
252
254
|
// ─── DOM ──────────────────────────────────────────────────────────────────────
|
|
255
|
+
function pageHasStableUuids() {
|
|
256
|
+
return !!document.documentElement.querySelector('[uuid]');
|
|
257
|
+
}
|
|
258
|
+
|
|
253
259
|
function applyDifferences(differences) {
|
|
260
|
+
if (!differences || typeof differences !== 'object') return;
|
|
261
|
+
|
|
254
262
|
Object.keys(differences).forEach(key => {
|
|
255
263
|
const diff = differences[key];
|
|
264
|
+
if (!diff) return;
|
|
256
265
|
|
|
266
|
+
// Root-level replace destroys <link rel="stylesheet"> / Bootstrap / scripts.
|
|
267
|
+
// Never rewrite documentElement.innerHTML — ignore and keep the live DOM.
|
|
257
268
|
if (!diff.parent) {
|
|
258
269
|
if (diff.status === 'Changed') {
|
|
259
|
-
|
|
260
|
-
|
|
270
|
+
console.warn(
|
|
271
|
+
'PyWeber: ignored root document replace (would break CSS/scripts). ' +
|
|
272
|
+
'Use include_uuid=True for reactive updates.'
|
|
273
|
+
);
|
|
261
274
|
}
|
|
262
275
|
return;
|
|
263
276
|
}
|
|
264
277
|
|
|
265
278
|
const parentElement = document.querySelector(`[uuid="${diff.parent}"]`);
|
|
266
|
-
if (!parentElement)
|
|
279
|
+
if (!parentElement) {
|
|
280
|
+
// UUID contract broken (e.g. stamped client uuids vs server) — skip, do not invent a full replace
|
|
281
|
+
return;
|
|
282
|
+
}
|
|
267
283
|
|
|
268
284
|
switch (diff.status) {
|
|
269
285
|
case 'Added':
|
|
@@ -286,6 +302,7 @@ function applyDifferences(differences) {
|
|
|
286
302
|
if (toChange) {
|
|
287
303
|
toChange.parentNode.replaceChild(createElementFromHTML(diff.element), toChange);
|
|
288
304
|
}
|
|
305
|
+
// missing uuid target → ignore (do not fall back to full-document rewrite)
|
|
289
306
|
}
|
|
290
307
|
break;
|
|
291
308
|
}
|
|
@@ -377,9 +394,12 @@ function getHandoffToken() {
|
|
|
377
394
|
return document.querySelector('meta[name="pyweber-handoff"]')?.content || null;
|
|
378
395
|
}
|
|
379
396
|
|
|
380
|
-
/** Atribui uuid a nós injectados por JS (ou libs externas) antes do sync com o servidor.
|
|
397
|
+
/** Atribui uuid a nós injectados por JS (ou libs externas) antes do sync com o servidor.
|
|
398
|
+
* Só corre se a página já tiver o contrato uuid (include_uuid=True). Nunca inventa
|
|
399
|
+
* um universo de UUIDs em páginas estáticas — isso parte o handoff/diff. */
|
|
381
400
|
function stampMissingUuids(root = document.documentElement) {
|
|
382
401
|
if (!(root instanceof Element)) return;
|
|
402
|
+
if (!pageHasStableUuids()) return;
|
|
383
403
|
|
|
384
404
|
const nodes = root === document.documentElement
|
|
385
405
|
? root.querySelectorAll('*')
|
|
@@ -395,6 +415,7 @@ function stampMissingUuids(root = document.documentElement) {
|
|
|
395
415
|
/** Re-sincroniza DOM → servidor depois de JS injectar HTML (sem uuid) após o WS abrir. */
|
|
396
416
|
async function resyncDomWithServer() {
|
|
397
417
|
if (!socketReady || socket.readyState !== WebSocket.OPEN) return;
|
|
418
|
+
if (!pageHasStableUuids()) return;
|
|
398
419
|
stampMissingUuids();
|
|
399
420
|
await sendToServer(await getEventData({ includeTemplate: true }));
|
|
400
421
|
}
|
|
@@ -110,6 +110,40 @@ def csrf_enabled() -> bool:
|
|
|
110
110
|
return bool(value)
|
|
111
111
|
|
|
112
112
|
|
|
113
|
+
# Allows HTTPS CDNs (Bootstrap, Google Fonts, jsDelivr icons) while still
|
|
114
|
+
# blocking exotic schemes and clickjacking. Override with PYWEBER_CSP /
|
|
115
|
+
# [security].csp; set to "off" to omit the header.
|
|
116
|
+
DEFAULT_CSP = (
|
|
117
|
+
"default-src 'self'; "
|
|
118
|
+
"script-src 'self' 'unsafe-inline' https:; "
|
|
119
|
+
"style-src 'self' 'unsafe-inline' https:; "
|
|
120
|
+
"font-src 'self' data: https:; "
|
|
121
|
+
"img-src 'self' data: https:; "
|
|
122
|
+
"connect-src 'self' ws: wss: https:; "
|
|
123
|
+
"frame-ancestors 'none'; "
|
|
124
|
+
"base-uri 'self'; "
|
|
125
|
+
"form-action 'self'"
|
|
126
|
+
)
|
|
127
|
+
|
|
128
|
+
_CSP_OFF = frozenset({'', '0', 'false', 'off', 'none', 'disable', 'disabled'})
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def resolve_csp() -> str | None:
|
|
132
|
+
"""Return CSP header value, or None to omit the header.
|
|
133
|
+
|
|
134
|
+
Precedence: ``PYWEBER_CSP`` env → ``[security].csp`` config → ``DEFAULT_CSP``.
|
|
135
|
+
"""
|
|
136
|
+
raw = os.environ.get('PYWEBER_CSP')
|
|
137
|
+
if raw is None:
|
|
138
|
+
raw = _config().get('security', 'csp', default=None)
|
|
139
|
+
if raw is None:
|
|
140
|
+
return DEFAULT_CSP
|
|
141
|
+
value = str(raw).strip()
|
|
142
|
+
if value.lower() in _CSP_OFF:
|
|
143
|
+
return None
|
|
144
|
+
return value
|
|
145
|
+
|
|
146
|
+
|
|
113
147
|
def get_allowed_origins() -> set[str]:
|
|
114
148
|
origins = _config().get('security', 'allowed_origins', default=None)
|
|
115
149
|
if origins is None:
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|