From 6067338932a237fba32776c5de5ad83e0dc5657b Mon Sep 17 00:00:00 2001 From: hujambo-dunia Date: Tue, 4 Feb 2025 09:55:54 -0500 Subject: [PATCH] Added changes to files that touch the SMTP process for new bug report type --- .../Collections/common/UserReportingError.vue | 4 +- .../Collections/common/reporting.ts | 4 +- lib/galaxy/schema/schema.py | 8 + lib/galaxy/tools/errors.py | 176 +++++++++++------- lib/galaxy/webapps/galaxy/api/tools.py | 12 +- 5 files changed, 125 insertions(+), 79 deletions(-) diff --git a/client/src/components/Collections/common/UserReportingError.vue b/client/src/components/Collections/common/UserReportingError.vue index e64ceb2c4fad..e2b9617373e8 100644 --- a/client/src/components/Collections/common/UserReportingError.vue +++ b/client/src/components/Collections/common/UserReportingError.vue @@ -2,7 +2,7 @@ import { library } from "@fortawesome/fontawesome-svg-core"; import { faBug } from "@fortawesome/free-solid-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/vue-fontawesome"; -import { BAlert, BButton, BCollapse, BLink } from "bootstrap-vue"; +import { BAlert, BButton, BLink, BCollapse } from "bootstrap-vue"; import { computed, ref } from "vue"; import { dispatchReport, type ReportType } from "@/components/Collections/common/reporting"; @@ -39,7 +39,7 @@ const fieldMessages = computed(() => Boolean ) ); -const expandedIcon = computed(() => (isExpanded.value ? "-" : "+")); +const expandedIcon = computed(() => (isExpanded.value ? '-' : '+')); async function handleSubmit(reportType: ReportType, data?: any, email?: string | null) { if (!data || !email) { diff --git a/client/src/components/Collections/common/reporting.ts b/client/src/components/Collections/common/reporting.ts index b2571fd4156c..90eb8f979b29 100644 --- a/client/src/components/Collections/common/reporting.ts +++ b/client/src/components/Collections/common/reporting.ts @@ -65,10 +65,8 @@ export async function submitReportTool( if (error) { return { messages: [], error: errorMessageAsString(error) }; } - // return { messages: data.messages }; - return { messages: [["Success!", "success"]] }; + return { messages: data.messages }; } catch (err) { - console.log("api error (err)", err); return { messages: [], error: errorMessageAsString(err) }; } } diff --git a/lib/galaxy/schema/schema.py b/lib/galaxy/schema/schema.py index e33053fed0d0..d7c88af4fa12 100644 --- a/lib/galaxy/schema/schema.py +++ b/lib/galaxy/schema/schema.py @@ -3928,3 +3928,11 @@ def __get_pydantic_core_schema__(cls, source_type, handler): core_schema.str_schema(), serialization=core_schema.to_string_ser_schema(), ) + + +class ToolErrorSummary(Model): + messages: List[List[str]] = Field( + default=..., + title="Error messages", + description="The error messages for the specified tool.", + ) diff --git a/lib/galaxy/tools/errors.py b/lib/galaxy/tools/errors.py index f6db3d1dfa3d..1e60341362f8 100644 --- a/lib/galaxy/tools/errors.py +++ b/lib/galaxy/tools/errors.py @@ -44,7 +44,7 @@ def _can_access_dataset(self, user): return self.app.security_agent.can_access_dataset(roles, self.hda.dataset) def create_report(self, user, email="", message="", redact_user_details_in_bugreport=False, **kwd): - email_str = _redact_email(user, email, redact_user_details_in_bugreport) + email_str = ReportPresenter._redact_email(user, email, redact_user_details_in_bugreport) hda = self.hda job = self.job host = self.app.url_for("/", qualified=True) @@ -81,14 +81,8 @@ def create_report(self, user, email="", message="", redact_user_details_in_bugre email_str=email_str, message=util.unicodify(message), ) - self.report = templates.render(REPORT_TEMPLATE_DATASET_TXT, report_variables, self.app.config.templates_dir) - - # Escape all of the content for use in the HTML report - for parameter in report_variables.keys(): - if report_variables[parameter] is not None: - report_variables[parameter] = markupsafe.escape(unicodify(report_variables[parameter])) - - self.html_report = templates.render(REPORT_TEMPLATE_DATASET_HTML, report_variables, self.app.config.templates_dir) + self.report = ReportPresenter._get_body(self, REPORT_TEMPLATE_DATASET_TXT, self.app.config.templates_dir, report_variables) + self.html_report = ReportPresenter._get_body(self, REPORT_TEMPLATE_DATASET_HTML, self.app.config.templates_dir, ReportPresenter._escape_html(report_variables)) def _send_report(self, user, email=None, message=None, **kwd): return self.report @@ -136,63 +130,109 @@ def create_report_tool( redact_user_details_in_bugreport=False, **kwd ): - email_str = _redact_email(user, email, redact_user_details_in_bugreport) - history_id = self.history.id - history_id_encoded=self.app.security.encode_id(history_id) - job_tool_id = reportable_data.get("tool_id", None) - tool_version = reportable_data.get("tool_version", None) - report_variables = dict( - host=self.app.url_for("/", qualified=True), - history_id=history_id, - history_id_encoded=history_id_encoded, - history_view_link=self.app.url_for("/histories/view", id=history_id_encoded, qualified=True), - job_tool_id=job_tool_id, - job_tool_version=tool_version, - transcript=json.dumps(reportable_data, indent=4, ensure_ascii=False), - # TODO are there any errors that can be captured and... - # ...displayed here ? Even browser ones ? Previously... - # ...we could capture: job_stderr, job_stdout, job_info,... - # ...job_traceback - email_str=email_str, - message=util.unicodify(message), - ) - self.report = templates.render(REPORT_TEMPLATE_TOOL_TXT, report_variables, self.app.config.templates_dir) - - # Escape all of the content for use in the HTML report - for parameter in report_variables.keys(): - if report_variables[parameter] is not None: - report_variables[parameter] = markupsafe.escape(unicodify(report_variables[parameter])) - - self.html_report = templates.render(REPORT_TEMPLATE_TOOL_HTML, report_variables, self.app.config.templates_dir) - - error_reporter = EmailErrorReporter(self.hda, self.app) - error_reporter.create_report(user, email=email, message=message, redact_user_details_in_bugreport=redact_user_details_in_bugreport, **kwd) - return error_reporter - - -def _redact_email(user, email=None, redact_user_details_in_bugreport=False) -> str: - if redact_user_details_in_bugreport: - # This is sub-optimal but it is hard to solve fully. This affects - # the GitHub posting method more than the traditional email plugin. - # There is no way around CCing the person with the traditional - # email bug report plugin, however with the GitHub plugin we can - # submit to GitHub without putting the email in the bug report. - # - # A secondary system with access to the GitHub issue and access to - # the Galaxy database can shuttle email back and forth between - # GitHub comments and user-emails. - # Thus preventing issue helpers from every knowing the identity of - # the bug reporter (and preventing information about the bug - # reporter from leaving the EU until it hits email directly to the - # user.) - email_str = "redacted" - if user: - email_str += f" (user: {user.id})" - else: - if user: - email_str = f"'{user.email}'" - if email and user.email != email: - email_str += f" (providing preferred contact email '{email}')" + try: + email_str = ReportPresenter._redact_email(user, email, redact_user_details_in_bugreport) + history_id = self.history.id + history_id_encoded=self.app.security.encode_id(history_id) + job_tool_id = reportable_data.get("tool_id", None) + tool_version = reportable_data.get("tool_version", None) + report_variables = dict( + host=self.app.url_for("/", qualified=True), + history_id=history_id, + history_id_encoded=history_id_encoded, + history_view_link=self.app.url_for("/histories/view", id=history_id_encoded, qualified=True), + job_tool_id=job_tool_id, + job_tool_version=tool_version, + transcript=json.dumps(reportable_data, indent=4, ensure_ascii=False), + email_str=email_str, + message=util.unicodify(message), + ) + body = templates.render(REPORT_TEMPLATE_TOOL_TXT, report_variables, self.app.config.templates_dir) + html = templates.render(REPORT_TEMPLATE_TOOL_HTML, ReportPresenter._escape_html(report_variables), self.app.config.templates_dir) + subject = ReportPresenter._get_subject(self, email, job_tool_id, tool_version) + email_to = ReportPresenter._get_email_to(email, self.app.config.error_email_to) + ReportEmailer().send_report(user, email=email_to, message=message, subject=subject, body=body, html=html) + return [["Your error report has been sent", "success"]] + except Exception as e: + msg = f"An error occurred sending the report by email: {unicodify(e)}" + return (msg, "danger") + + +class ReportPresenter: + def _get_email_to(email, error_email_to): + error_msg = validate_email_str(email) + if not error_msg: + return f"{error_email_to}, {email.strip()}" + return error_email_to + + def _get_subject(self, email, tool_id, tool_version): + subject = f"Galaxy tool error report from {email}" + try: + subject = f"{subject} ({self.app.toolbox.get_tool(tool_id, tool_version).old_id})" + except Exception: + pass + return subject + + def _get_body(self, template_file, template_directory, template_variables): + return templates.render(template_file, template_variables, template_directory) + + def _escape_html(content): + # Escape all of the dynamic-content for use in the HTML report + for parameter in content.keys(): + if content[parameter] is not None: + content[parameter] = markupsafe.escape(unicodify(content[parameter])) + return content + + def _redact_email(user, email=None, redact_user_details_in_bugreport=False) -> str: + if redact_user_details_in_bugreport: + # This is sub-optimal but it is hard to solve fully. This affects + # the GitHub posting method more than the traditional email plugin. + # There is no way around CCing the person with the traditional + # email bug report plugin, however with the GitHub plugin we can + # submit to GitHub without putting the email in the bug report. + # + # A secondary system with access to the GitHub issue and access to + # the Galaxy database can shuttle email back and forth between + # GitHub comments and user-emails. + # Thus preventing issue helpers from every knowing the identity of + # the bug reporter (and preventing information about the bug + # reporter from leaving the EU until it hits email directly to the + # user.) + email_str = "redacted" + if user: + email_str += f" (user: {user.id})" else: - email_str = "'%s'" % (email or "anonymous") - return email_str + if user: + email_str = f"'{user.email}'" + if email and user.email != email: + email_str += f" (providing preferred contact email '{email}')" + else: + email_str = "'%s'" % (email or "anonymous") + return email_str + +class ReportEmailer: + def send_report(self, user, email=None, message=None, **kwd): + smtp_server = self.app.config.smtp_server + assert smtp_server, ValueError("Mail is not configured for this Galaxy instance") + to = self.app.config.error_email_to + assert to, ValueError("Error reporting has been disabled for this Galaxy instance") + + error_msg = validate_email_str(email) + if not error_msg and self._can_access_dataset(user): + to += f", {email.strip()}" + subject = f"Galaxy tool error report from {email}" + try: + subject = f"{subject} ({self.app.toolbox.get_tool(self.job.tool_id, self.job.tool_version).old_id})" + except Exception: + pass + + reply_to = user.email if user else None + return util.send_mail( + self.app.config.email_from, + to, + subject, + self.report, + self.app.config, + html=self.html_report, + reply_to=reply_to, + ) diff --git a/lib/galaxy/webapps/galaxy/api/tools.py b/lib/galaxy/webapps/galaxy/api/tools.py index 101f72948a52..2b15bcacc710 100644 --- a/lib/galaxy/webapps/galaxy/api/tools.py +++ b/lib/galaxy/webapps/galaxy/api/tools.py @@ -59,6 +59,9 @@ Router, ) from galaxy.tools.errors import EmailErrorReporterTool +from galaxy.schema.schema import ( + ToolErrorSummary +) log = logging.getLogger(__name__) @@ -122,21 +125,19 @@ class EmailReportTools: "/api/tools/{tool_id}/error", public=True, summary="Get tool error details", - response_model=None, ) def error( self, trans: ProvidesUserContext = DependsOnTrans, - tool_id: str = Path(..., description="The ID of the tool"), payload: dict = Body(..., description="The Payload from the form"), - ) -> Any: + ) -> ToolErrorSummary: args = _kwd_or_payload(payload) email = args.get("email", None) message = args.get("message", None) reportable_data = args.get("reportable_data", None) try: error_reporter = EmailErrorReporterTool - error_reporter.create_report_tool( + response = error_reporter.create_report_tool( self=trans, user=trans.user, reportable_data=reportable_data, @@ -144,10 +145,9 @@ def error( message=message, redact_user_details_in_bugreport=trans.app.config.redact_user_details_in_bugreport, ) - return ("Your error report has been sent", "success") + return ToolErrorSummary(messages=response) except Exception as e: msg = f"An error occurred sending the report by email: {unicodify(e)}" - log.info(msg) return (msg, "danger")