Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .git-blame-ignore-revs
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
# Mass reformats (style: PRs), one squash SHA per line with its subject as a comment.
# GitHub reads this file; locally the dev shell sets blame.ignoreRevsFile.
0f2bacb5c3f826e683a21223e4adf81ad0eba101 # style: adopt the app standards (managed config and formatting) (#7)
10 changes: 1 addition & 9 deletions .oxlintrc.json
Original file line number Diff line number Diff line change
Expand Up @@ -54,15 +54,7 @@
}
},
{
"files": [
"esign/public/js/controls/font_select.js",
"esign/public/js/controls/signature.js",
"esign/public/js/controls/upload.js",
"esign/public/js/controls/utils.js",
"esign/public/js/fonts.js",
"esign/public/js/web/esign_webform_script.js",
"esign/public/js/web_form.js"
],
"files": ["esign/public/js/web_form.js"],
"globals": {
"frappe": "readonly",
"__": "readonly",
Expand Down
40 changes: 28 additions & 12 deletions esign/esign/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,23 @@
import json

import frappe

# Imported as a module and looked up at call time, so a patched get_attachments (cloud_storage
# replaces it) is the one called.
import frappe.desk.form.load
from frappe import _
from frappe.core.doctype.communication.communication import Communication
from frappe.utils import now
from frappe.utils import escape_html, now

# Communication type for eSign audit trail
ESIGN_COMMUNICATION_TYPE = "eSign"


def _escape(value: object) -> str | None:
"""A stored audit value, HTML-escaped for the timeline card (None stays None)."""
return None if value is None else escape_html(str(value))


def get_timeline_content(doctype: str, docname: str) -> list[dict]:
"""Return eSign audit trail entries for the document timeline.

Expand Down Expand Up @@ -48,7 +57,7 @@ def get_timeline_content(doctype: str, docname: str) -> list[dict]:
# Get the attached signed PDF file URL using the patched get_attachments
# This works for both standard Frappe and cloud_storage setups
file_url = None
attachments = frappe.desk.form.load.get_attachments("Communication", comm.name) # type: ignore
attachments = frappe.desk.form.load.get_attachments("Communication", comm.name)
for attachment in attachments:
if attachment.get("file_name", "").endswith(".pdf"):
file_url = attachment.get("file_url")
Expand All @@ -60,18 +69,21 @@ def get_timeline_content(doctype: str, docname: str) -> list[dict]:
"is_card": True,
"creation": comm.communication_date or comm.creation,
"template": "esign_timeline",
# The timeline template prints these unescaped ({{ }} is raw in frappe's
# microtemplates), and the signer controls several of them (user agent, email
# parameter, form fields), so every value is escaped here.
"template_data": {
"creation": str(comm.creation),
"file_url": file_url,
"signer_name": audit_data.get("signer_name") or comm.sender_full_name,
"signer_email": audit_data.get("signer_email") or comm.sender,
"timestamp": audit_data.get("timestamp"),
"ip_address": audit_data.get("ip_address"),
"user_agent": audit_data.get("user_agent"),
"web_form": audit_data.get("web_form"),
"print_format": audit_data.get("print_format"),
"signed_fields": audit_data.get("signed_fields", []),
"pdf_hash": audit_data.get("pdf_hash"),
"file_url": _escape(file_url),
"signer_name": _escape(audit_data.get("signer_name") or comm.sender_full_name),
"signer_email": _escape(audit_data.get("signer_email") or comm.sender),
"timestamp": _escape(audit_data.get("timestamp")),
"ip_address": _escape(audit_data.get("ip_address")),
"user_agent": _escape(audit_data.get("user_agent")),
"web_form": _escape(audit_data.get("web_form")),
"print_format": _escape(audit_data.get("print_format")),
"signed_fields": [_escape(field) for field in audit_data.get("signed_fields") or []],
"pdf_hash": _escape(audit_data.get("pdf_hash")),
},
}
)
Expand Down Expand Up @@ -126,6 +138,10 @@ def send_esign_email(
except frappe.DoesNotExistError:
frappe.throw(_("Document {0} {1} not found").format(doctype, name))

# The email carries a share key that opens the document to its recipients, so the sender
# needs the same permission frappe's own email dialog asks for.
frappe.has_permission(doctype, ptype="email", doc=name, throw=True)

# Validate web form
from esign.esign.custom.web_form import EsignWebForm, get_esign_link

Expand Down
Loading