This is an automated email from the ASF dual-hosted git repository. diegopucci pushed a commit to branch geido/fix/dashboard-pdf-download in repository https://gitbox.apache.org/repos/asf/superset.git
commit cdbf70d9a3496e2ba1128e8d2966c7da3cf28501 Author: Diego Pucci <[email protected]> AuthorDate: Fri Apr 26 17:48:00 2024 +0300 Add img proxy --- superset-frontend/src/utils/downloadAsImage.ts | 5 ++ superset-frontend/src/utils/downloadAsPdf.ts | 1 + superset/initialization/__init__.py | 2 + superset/views/img_proxy.py | 64 ++++++++++++++++++++++++++ 4 files changed, 72 insertions(+) diff --git a/superset-frontend/src/utils/downloadAsImage.ts b/superset-frontend/src/utils/downloadAsImage.ts index 63b65cb60b..3bc8bca130 100644 --- a/superset-frontend/src/utils/downloadAsImage.ts +++ b/superset-frontend/src/utils/downloadAsImage.ts @@ -72,6 +72,11 @@ export default function downloadAsImage( .toJpeg(elementToPrint, { bgcolor: supersetTheme.colors.grayscale.light4, filter, + corsImg: { + url: '/img_proxy/?url=#{cors}', + method: 'GET', + data: '', + }, }) .then((dataUrl: string) => { const link = document.createElement('a'); diff --git a/superset-frontend/src/utils/downloadAsPdf.ts b/superset-frontend/src/utils/downloadAsPdf.ts index bb769d1eb1..c9fd462e06 100644 --- a/superset-frontend/src/utils/downloadAsPdf.ts +++ b/superset-frontend/src/utils/downloadAsPdf.ts @@ -62,6 +62,7 @@ export default function downloadAsPdf( image: { type: 'jpeg', quality: 1 }, html2canvas: { scale: 2 }, excludeClassNames: ['header-controls'], + proxyUrl: '/img_proxy/?url=', }; return domToPdf(elementToPrint, options) .then(() => { diff --git a/superset/initialization/__init__.py b/superset/initialization/__init__.py index 069ed19483..af53db2826 100644 --- a/superset/initialization/__init__.py +++ b/superset/initialization/__init__.py @@ -174,6 +174,7 @@ class SupersetAppInitializer: # pylint: disable=too-many-public-methods from superset.views.datasource.views import DatasetEditor, Datasource from superset.views.dynamic_plugins import DynamicPluginsView from superset.views.explore import ExplorePermalinkView, ExploreView + from superset.views.img_proxy import ImgProxyView from superset.views.key_value import KV from superset.views.log.api import LogRestApi from superset.views.log.views import LogModelView @@ -314,6 +315,7 @@ class SupersetAppInitializer: # pylint: disable=too-many-public-methods appbuilder.add_view_no_menu(TaggedObjectsModelView) appbuilder.add_view_no_menu(TagView) appbuilder.add_view_no_menu(ReportView) + appbuilder.add_view_no_menu(ImgProxyView()) # # Add links diff --git a/superset/views/img_proxy.py b/superset/views/img_proxy.py new file mode 100644 index 0000000000..58ade4246a --- /dev/null +++ b/superset/views/img_proxy.py @@ -0,0 +1,64 @@ +from typing import Any, Dict +import requests +from urllib.parse import urlparse +from flask import g, request, Response +from flask_appbuilder.api import expose +from superset import event_logger +from superset.utils.core import ( + get_user_id, +) +from .base import BaseSupersetView + +class ImgProxyView(BaseSupersetView): + route_base = "/img_proxy" + + @expose("/") + @event_logger.log_this + def img_proxy(self) -> Response: + """ + Proxy to an external URL, to overcome CORS restrictions. + Returns a HTTP response containing the resource fetched from the external URL. + """ + if not get_user_id(): + raise Exception("User context not found") + + url = request.args.get('url') + + if not url: + return Response("URL parameter 'url' is missing", status=400) + + parsed_url = urlparse(url) + if parsed_url.scheme not in ['http', 'https']: + return Response("Invalid URL scheme", status=400) + + try: + response = self.fetch_resource(url) + except Exception: + return Response("Error fetching resource", status=500) + + return self.build_response(response) + + def fetch_resource(self, url: str) -> Any: + """Fetch the resource from the external server and handle errors.""" + try: + response = requests.get(url) + response.raise_for_status() + + return response + except requests.RequestException as e: + raise e + + def build_response(self, response: requests.Response) -> Response: + """Build the HTTP response to return based on the fetched resource.""" + allowed_content_types = ['image/'] + content_type = response.headers.get('content-type', '') + + if not any(content_type.startswith(content_type_prefix) for content_type_prefix in allowed_content_types): + return Response("Response is not an allowed resource type", status=400) + + headers: Dict[str, Any] = {key: value for (key, value) in response.headers.items()} + + return Response(response.content, response.status_code, headers) + + +
