Skip to content

Commit 82c6329

Browse files
committed
Merge remote-tracking branch 'origin/19.0' into feat/cycle-compliance-on-registrant
# Conflicts: # spp_programs/tests/__init__.py
2 parents 5d9edc4 + b061135 commit 82c6329

83 files changed

Lines changed: 6801 additions & 1952 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

spp

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,14 @@ import shutil
2222
import subprocess
2323
import sys
2424
import time
25-
import tomllib
25+
26+
try:
27+
import tomllib
28+
except ModuleNotFoundError:
29+
try:
30+
import tomli as tomllib
31+
except ModuleNotFoundError:
32+
tomllib = None
2633
from pathlib import Path
2734

2835
try:
@@ -99,13 +106,15 @@ DEFAULT_CONFIG = {
99106
def load_config() -> dict:
100107
"""Load config from ~/.spp.toml, returning defaults if not found."""
101108
config = DEFAULT_CONFIG.copy()
102-
if CONFIG_PATH.exists():
109+
if CONFIG_PATH.exists() and tomllib is not None:
103110
try:
104111
with open(CONFIG_PATH, "rb") as f:
105112
user_config = tomllib.load(f)
106113
config.update(user_config)
107114
except (tomllib.TOMLDecodeError, OSError) as e:
108115
warn(f"Could not load config from {CONFIG_PATH}: {e}")
116+
elif CONFIG_PATH.exists() and tomllib is None:
117+
warn("tomllib not available (Python < 3.11). Install 'tomli' or upgrade Python to load ~/.spp.toml config.")
109118
return config
110119

111120

@@ -131,7 +140,7 @@ DEMO_PROFILES = {
131140
}
132141

133142

134-
def run(cmd: str | list, check: bool = True, capture: bool = False, **kwargs) -> subprocess.CompletedProcess:
143+
def run(cmd, check: bool = True, capture: bool = False, **kwargs) -> subprocess.CompletedProcess:
135144
"""Run a command with nice defaults."""
136145
if isinstance(cmd, str):
137146
kwargs.setdefault("shell", True)
@@ -909,7 +918,7 @@ def cmd_sql(args):
909918
run(docker_compose("exec", "db", "psql", "-U", "odoo", "-d", db_name))
910919

911920

912-
def _show_url(open_browser: bool = False) -> str | None:
921+
def _show_url(open_browser: bool = False):
913922
"""Get the running Odoo server URL."""
914923
service, profile = _find_running_odoo()
915924
if not service:

spp_api_v2/tests/test_search_service.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -57,8 +57,8 @@ def test_search_by_name(self):
5757
self.assertEqual(total, 2)
5858
self.assertEqual(len(records), 2)
5959
names = [r.name for r in records]
60-
self.assertIn("Alice Johnson", names)
61-
self.assertIn("Alice Brown", names)
60+
self.assertIn("JOHNSON, ALICE", names)
61+
self.assertIn("BROWN, ALICE", names)
6262

6363
def test_parse_identifier_param(self):
6464
"""identifier=system|value creates proper domain"""
@@ -224,7 +224,7 @@ def test_search_combined_params(self):
224224
# Should find Alice Johnson and Alice Brown (both female)
225225
self.assertEqual(total, 2)
226226
for record in records:
227-
self.assertIn("Alice", record.name)
227+
self.assertIn("ALICE", record.name)
228228
self.assertEqual(record.gender_id, self.gender_female)
229229

230230

spp_audit/README.rst

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -154,6 +154,8 @@ Changelog
154154
all records, not just the first
155155
- refactor: use ``isinstance(value, Markup)`` instead of fragile
156156
``str(type(...))`` comparison in all audit decorator methods
157+
- fix: add HTML escaping to computed ``data_html`` and
158+
``parent_data_html`` fields to prevent stored XSS (#50)
157159

158160
19.0.2.0.0
159161
~~~~~~~~~~

spp_audit/models/spp_audit_log.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
import logging
33

44
from dateutil import tz
5+
from markupsafe import escape as html_escape
56

67
from odoo import _, api, fields, models
78
from odoo.exceptions import UserError
@@ -164,7 +165,7 @@ def _compute_data_html(self):
164165
for line in rec._get_content():
165166
row = ""
166167
for item in line:
167-
row += f"<td>{item}</td>"
168+
row += f"<td>{html_escape(str(item))}</td>"
168169
tbody += f"<tr>{row}</tr>"
169170
tbody = f"<tbody>{tbody}</tbody>"
170171
rec.data_html = f'<table class="o_list_view table table-condensed table-striped">{thead}{tbody}</table>'
@@ -206,7 +207,7 @@ def _compute_parent_data_html(self):
206207
for line in rec._parent_get_content():
207208
row = ""
208209
for item in line:
209-
row += f"<td>{item}</td>"
210+
row += f"<td>{html_escape(str(item))}</td>"
210211
tbody += f"<tr>{row}</tr>"
211212
tbody = f"<tbody>{tbody}</tbody>"
212213
rec.parent_data_html = (

spp_audit/readme/HISTORY.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
- fix: use @api.model_create_multi for audit_create to support Odoo 19 create overrides (#138)
44
- fix: Markup sanitization in audit_write and audit_unlink now handles all records, not just the first
55
- refactor: use `isinstance(value, Markup)` instead of fragile `str(type(...))` comparison in all audit decorator methods
6+
- fix: add HTML escaping to computed `data_html` and `parent_data_html` fields to prevent stored XSS (#50)
67

78
### 19.0.2.0.0
89

spp_audit/static/description/index.html

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -524,6 +524,8 @@ <h1>19.0.2.0.1</h1>
524524
all records, not just the first</li>
525525
<li>refactor: use <tt class="docutils literal">isinstance(value, Markup)</tt> instead of fragile
526526
<tt class="docutils literal"><span class="pre">str(type(...))</span></tt> comparison in all audit decorator methods</li>
527+
<li>fix: add HTML escaping to computed <tt class="docutils literal">data_html</tt> and
528+
<tt class="docutils literal">parent_data_html</tt> fields to prevent stored XSS (#50)</li>
527529
</ul>
528530
</div>
529531
<div class="section" id="section-2">

spp_audit/tests/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,3 @@
11
from . import test_audit_backend
2+
from . import test_html_escaping
23
from . import test_spp_audit_rule
Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
from odoo.tests.common import TransactionCase
2+
3+
4+
class TestAuditHtmlEscaping(TransactionCase):
5+
"""Tests that audit log HTML fields properly escape dynamic values."""
6+
7+
@classmethod
8+
def setUpClass(cls):
9+
super().setUpClass()
10+
cls.model_partner = cls.env["ir.model"].search([("model", "=", "res.partner")], limit=1)
11+
cls.audit_rule = cls.env["spp.audit.rule"].search([("model_id", "=", cls.model_partner.id)], limit=1)
12+
if not cls.audit_rule:
13+
cls.audit_rule = cls.env["spp.audit.rule"].create(
14+
{
15+
"name": "Test Rule",
16+
"model_id": cls.model_partner.id,
17+
"is_log_create": True,
18+
"is_log_write": True,
19+
"is_log_unlink": False,
20+
}
21+
)
22+
23+
def _create_audit_log(self, old_vals, new_vals):
24+
"""Create an audit log record with given old/new values."""
25+
data = repr({"old": old_vals, "new": new_vals})
26+
return self.env["spp.audit.log"].create(
27+
{
28+
"audit_rule_id": self.audit_rule.id,
29+
"user_id": self.env.uid,
30+
"model_id": self.model_partner.id,
31+
"res_id": 1,
32+
"method": "write",
33+
"data": data,
34+
}
35+
)
36+
37+
def test_data_html_escapes_script_tags(self):
38+
"""Verify data_html escapes <script> in field values."""
39+
xss_payload = '<script>alert("xss")</script>'
40+
log = self._create_audit_log(
41+
old_vals={"name": "Safe Name"},
42+
new_vals={"name": xss_payload},
43+
)
44+
html = log.data_html
45+
self.assertNotIn("<script>", html)
46+
self.assertIn("&lt;script&gt;", html)
47+
48+
def test_data_html_escapes_html_entities(self):
49+
"""Verify data_html escapes angle brackets and ampersands."""
50+
log = self._create_audit_log(
51+
old_vals={"name": "Before <b>bold</b>"},
52+
new_vals={"name": "After <img src=x onerror=alert(1)>"},
53+
)
54+
html = log.data_html
55+
self.assertNotIn("<img ", html)
56+
self.assertIn("&lt;img ", html)
57+
self.assertNotIn("<b>bold</b>", html)
58+
self.assertIn("&lt;b&gt;bold&lt;/b&gt;", html)
59+
60+
def test_data_html_renders_table_structure(self):
61+
"""Verify data_html still produces valid table structure."""
62+
log = self._create_audit_log(
63+
old_vals={"name": "Old"},
64+
new_vals={"name": "New"},
65+
)
66+
html = log.data_html
67+
self.assertIn("<table", html)
68+
self.assertIn("<thead>", html)
69+
self.assertIn("<tbody>", html)
70+
self.assertIn("<td>", html)
71+
72+
def test_parent_data_html_escapes_script_tags(self):
73+
"""Verify parent_data_html escapes <script> in field values."""
74+
xss_payload = '<script>alert("xss")</script>'
75+
parent_model = self.env["ir.model"].search([("model", "=", "res.partner")], limit=1)
76+
log = self.env["spp.audit.log"].create(
77+
{
78+
"audit_rule_id": self.audit_rule.id,
79+
"user_id": self.env.uid,
80+
"model_id": self.model_partner.id,
81+
"res_id": 1,
82+
"method": "write",
83+
"data": repr({"old": {"name": "Safe"}, "new": {"name": xss_payload}}),
84+
"parent_model_id": parent_model.id,
85+
"parent_res_ids_str": "1",
86+
}
87+
)
88+
html = log.parent_data_html
89+
self.assertNotIn("<script>", html)
90+
self.assertIn("&lt;script&gt;", html)

spp_case_demo/README.rst

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ OpenSPP Case Management Demo Data
77
!! This file is generated by oca-gen-addon-readme !!
88
!! changes will be overwritten. !!
99
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
10-
!! source digest: sha256:39196739031b49c0d69e329806b940da071ca49472f130cd28ffd45eda6459e7
10+
!! source digest: sha256:force_regen
1111
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
1212
1313
.. |badge1| image:: https://img.shields.io/badge/maturity-Production%2FStable-green.png
@@ -24,16 +24,20 @@ OpenSPP Case Management Demo Data
2424

2525
Demo data generator for Case Management system. Creates realistic cases
2626
with intervention plans, home visits, progress notes, and service
27-
referrals. Includes 9 fixed demo stories for training and sales demos,
28-
plus configurable random case generation for volume testing.
27+
referrals. Includes 9 fixed demo stories plus 3 background cases for
28+
training and sales demos, and configurable volume case generation using
29+
Faker for locale-aware random data (non-deterministic — each run
30+
produces different results).
2931

3032
Key Capabilities
3133
~~~~~~~~~~~~~~~~
3234

3335
- Generate 9 fixed demo stories with predictable personas and case
34-
progressions for consistent training scenarios
35-
- Create random volume cases with configurable distribution percentages
36-
for plans, visits, notes, and closures
36+
progressions for consistent training scenarios, plus 3 background
37+
cases (Fernandez Intake Pending, Johnson Assessment, Kim Case Closed)
38+
for variety
39+
- Create random volume cases using Faker (non-seeded) with configurable
40+
distribution percentages for plans, visits, notes, and closures
3741
- Link generated cases to existing registrants or create standalone
3842
cases
3943
- Backdate case records and related activities to simulate realistic

spp_case_demo/readme/DESCRIPTION.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
1-
Demo data generator for Case Management system. Creates realistic cases with intervention plans, home visits, progress notes, and service referrals. Includes 9 fixed demo stories for training and sales demos, plus configurable random case generation for volume testing.
1+
Demo data generator for Case Management system. Creates realistic cases with intervention plans, home visits, progress notes, and service referrals. Includes 9 fixed demo stories plus 3 background cases for training and sales demos, and configurable volume case generation using Faker for locale-aware random data (non-deterministic — each run produces different results).
22

33
### Key Capabilities
44

5-
- Generate 9 fixed demo stories with predictable personas and case progressions for consistent training scenarios
6-
- Create random volume cases with configurable distribution percentages for plans, visits, notes, and closures
5+
- Generate 9 fixed demo stories with predictable personas and case progressions for consistent training scenarios, plus 3 background cases (Fernandez Intake Pending, Johnson Assessment, Kim Case Closed) for variety
6+
- Create random volume cases using Faker (non-seeded) with configurable distribution percentages for plans, visits, notes, and closures
77
- Link generated cases to existing registrants or create standalone cases
88
- Backdate case records and related activities to simulate realistic timelines over configurable day ranges
99
- Create intervention plans with multiple interventions across case lifecycle stages

0 commit comments

Comments
 (0)