From 1e0a0aa79d3de616c0538f7c7896977586cd06f3 Mon Sep 17 00:00:00 2001 From: Oliver Date: Sun, 22 Mar 2026 17:15:14 +1100 Subject: [PATCH] Add option to "asset" tag to control error raising (#11591) --- .../InvenTree/report/templatetags/report.py | 37 ++++++++++++++----- src/backend/InvenTree/report/test_tags.py | 15 ++++++-- 2 files changed, 40 insertions(+), 12 deletions(-) diff --git a/src/backend/InvenTree/report/templatetags/report.py b/src/backend/InvenTree/report/templatetags/report.py index 7b3b8ade62..8172f2c30f 100644 --- a/src/backend/InvenTree/report/templatetags/report.py +++ b/src/backend/InvenTree/report/templatetags/report.py @@ -188,20 +188,20 @@ def static_file_exists(path: Path | str) -> bool: def get_static_file_contents( - path: Path | str, raise_error: bool = True + path: Path | str, raise_error: bool = False ) -> bytes | None: """Return the contents of a static file. Arguments: path: The path to the static file, relative to the static storage root - raise_error: If True, raise an error if the file cannot be found (default = True) + raise_error: If True, raise an error if the file cannot be found (default = False) Returns: The contents of the static file, or None if the file cannot be found """ if not path: if raise_error: - raise ValueError('No media file specified') + raise ValueError('No static file specified') else: return None @@ -217,12 +217,14 @@ def get_static_file_contents( return file_data -def get_media_file_contents(path: Path | str, raise_error: bool = True) -> bytes | None: +def get_media_file_contents( + path: Path | str, raise_error: bool = False +) -> bytes | None: """Return the fully qualified file path to an uploaded media file. Arguments: path: The path to the media file, relative to the media storage root - raise_error: If True, raise an error if the file cannot be found (default = True) + raise_error: If True, raise an error if the file cannot be found (default = False) Returns: The contents of the media file, or None if the file cannot be found @@ -255,15 +257,24 @@ def get_media_file_contents(path: Path | str, raise_error: bool = True) -> bytes @register.simple_tag() -def asset(filename): +def asset(filename: str, raise_error: bool = False) -> str | None: """Return fully-qualified path for an upload report asset file. Arguments: filename: Asset filename (relative to the 'assets' media directory) + raise_error: If True, raise an error if the file cannot be found (default = False) Raises: FileNotFoundError: If file does not exist + ValueError: If an invalid filename is provided (e.g. empty string) + ValidationError: If the filename is invalid (e.g. path traversal attempt) """ + if not filename: + if raise_error: + raise ValueError('No asset file specified') + else: + return None + if type(filename) is SafeString: # Prepend an empty string to enforce 'stringiness' filename = '' + filename @@ -274,7 +285,10 @@ def asset(filename): full_path = Path('report', 'assets', filename) if not media_file_exists(full_path): - raise FileNotFoundError(_('Asset file not found') + f": '{filename}'") + if raise_error: + raise FileNotFoundError(_('Asset file not found') + f": '{filename}'") + else: + return None # In debug mode, return a web URL to the asset file (rather than a local file path) if get_global_setting('REPORT_DEBUG_MODE', cache=False): @@ -294,6 +308,7 @@ def uploaded_image( width: Optional[int] = None, height: Optional[int] = None, rotate: Optional[float] = None, + raise_error: bool = False, **kwargs, ) -> str: """Return raw image data from an 'uploaded' image. @@ -306,12 +321,14 @@ def uploaded_image( width: Optional width of the image height: Optional height of the image rotate: Optional rotation to apply to the image + raise_error: If True, raise an error if the file cannot be found (default = False) Returns: Binary image data to be rendered directly in a tag Raises: FileNotFoundError: If the file does not exist + ValueError: If an invalid filename is provided (e.g. empty string) """ if type(filename) is SafeString: # Prepend an empty string to enforce 'stringiness' @@ -330,7 +347,7 @@ def uploaded_image( raise FileNotFoundError(_('Image file not found') + f": '{filename}'") if exists: - img_data = get_media_file_contents(filename, raise_error=False) + img_data = get_media_file_contents(filename, raise_error=raise_error) # Check if the image data is valid if ( @@ -344,7 +361,9 @@ def uploaded_image( else: # Load the backup image from the static files directory replacement_file_path = Path('img', replacement_file) - img_data = get_static_file_contents(replacement_file_path) + img_data = get_static_file_contents( + replacement_file_path, raise_error=raise_error + ) if debug_mode: # In debug mode, return a web path (rather than an encoded image blob) diff --git a/src/backend/InvenTree/report/test_tags.py b/src/backend/InvenTree/report/test_tags.py index f055dea56f..abee2b09c8 100644 --- a/src/backend/InvenTree/report/test_tags.py +++ b/src/backend/InvenTree/report/test_tags.py @@ -61,7 +61,15 @@ class ReportTagTest(PartImageTestMixin, InvenTreeTestCase): self.debug_mode(b) with self.assertRaises(FileNotFoundError): - report_tags.asset('bad_file.txt') + report_tags.asset('bad_file.txt', raise_error=True) + + # Test for missing file, no error + self.assertIsNone(report_tags.asset('missing.txt')) + + self.assertIsNone(report_tags.asset('')) + + with self.assertRaises(ValueError): + report_tags.asset('', raise_error=True) # Create an asset file asset_dir = settings.MEDIA_ROOT.joinpath('report', 'assets') @@ -80,6 +88,7 @@ class ReportTagTest(PartImageTestMixin, InvenTreeTestCase): self.debug_mode(False) asset = report_tags.asset('test.txt') + self.assertEqual(asset, f'file://{settings.MEDIA_ROOT}/report/assets/test.txt') # Test for attempted path traversal with self.assertRaises(ValidationError): @@ -92,10 +101,10 @@ class ReportTagTest(PartImageTestMixin, InvenTreeTestCase): self.assertFalse(report_tags.static_file_exists(fn)) with self.assertRaises(FileNotFoundError): - report_tags.get_media_file_contents('dummy_file.txt') + report_tags.get_media_file_contents('dummy_file.txt', raise_error=True) with self.assertRaises(ValueError): - report_tags.get_static_file_contents(None) + report_tags.get_static_file_contents(None, raise_error=True) # Try again, without throwing an error self.assertIsNone(