From 47d3817c196f51f8b5c1c859b186747a7b9331c8 Mon Sep 17 00:00:00 2001 From: Lunga Baliwe Date: Mon, 13 Jul 2026 19:26:09 +0200 Subject: [PATCH] Fix loading of Dexterity images in PDF publisher Treat @@images URLs as local resources by traversing to their owning content object. The @@images browser view handles image field paths dynamically, so traversing the complete URL can return None and cause the publisher to incorrectly fetch the image externally. Continue using the complete image path for the authenticated subrequest and use the response Content-Type when available. --- src/senaite/impress/publisher.py | 36 +++++++++++++++++++++----------- 1 file changed, 24 insertions(+), 12 deletions(-) diff --git a/src/senaite/impress/publisher.py b/src/senaite/impress/publisher.py index 87c5e673..5a24ab99 100644 --- a/src/senaite/impress/publisher.py +++ b/src/senaite/impress/publisher.py @@ -145,7 +145,12 @@ def _layout_and_paginate(self, html): @synchronized(max_connections=2) def url_fetcher(self, url): - """Fetches internal URLs by path and not via an external request. + """Fetch internal URLs by path instead of via an external request. + + Browser views such as ``@@images`` handle parts of their paths + dynamically. Therefore, the owning content object is used when checking + whether an image resource is local, while the complete path is passed to + the authenticated subrequest. N.B. Multiple calls to this method might exhaust the available threads of the server, which causes a hanging instance. @@ -156,14 +161,19 @@ def url_fetcher(self, url): logger.info("Fetching URL '{}' for WeasyPrint".format(url)) - # get the pyhsical path from the URL request = api.get_request() + portal = api.get_portal() + host = request.get_header("HOST") path = "/".join(request.physicalPathFromURL(url)) - # fetch the object by sub-request - portal = api.get_portal() - context = portal.restrictedTraverse(path, None) + # The final field segment after @@images is handled dynamically by the + # images browser view and might not resolve with restrictedTraverse. + traverse_path = path + if "/@@images/" in path: + traverse_path = path.split("/@@images/", 1)[0] + + context = portal.restrictedTraverse(traverse_path, None) if context is None or (host and host not in url): logger.info("External URL, delegate to default URL fetcher...") @@ -171,22 +181,24 @@ def url_fetcher(self, url): logger.info("Local URL, fetching data by path '{}'".format(path)) - # get the data via an authenticated subrequest + # Fetch the local resource through an authenticated Zope subrequest. response = subrequest(path) - # Prepare the return data as required by WeasyPrint string = response.getBody() - filename = url.split("/")[-1] - mime_type = mimetypes.guess_type(url)[0] - redirected_url = url + filename = url.rstrip("/").split("/")[-1] + + # Dynamic URLs such as @@images/accreditation_body_logo have no file + # extension, so prefer the response's actual Content-Type. + mime_type = response.getHeader("Content-Type") + if not mime_type: + mime_type = mimetypes.guess_type(url)[0] return { "string": string, "filename": filename, "mime_type": mime_type, - "redirected_url": redirected_url, + "redirected_url": url, } - def write_png(self, html, resolution=96): """Write a PNG from the given HTML """