From 3c1db7881a3839b8752283a4c47a4f1a374f7aa2 Mon Sep 17 00:00:00 2001 From: Suraj Shetty Date: Tue, 21 Dec 2021 15:43:10 +0530 Subject: [PATCH 001/340] fix: Maintain document signature in Document Key table --- frappe/core/doctype/communication/mixins.py | 2 +- frappe/core/doctype/document_key/__init__.py | 0 .../core/doctype/document_key/document_key.js | 8 +++ .../doctype/document_key/document_key.json | 65 +++++++++++++++++++ .../core/doctype/document_key/document_key.py | 8 +++ .../doctype/document_key/test_document_key.py | 8 +++ frappe/model/document.py | 15 +++++ frappe/www/printview.py | 2 +- 8 files changed, 106 insertions(+), 2 deletions(-) create mode 100644 frappe/core/doctype/document_key/__init__.py create mode 100644 frappe/core/doctype/document_key/document_key.js create mode 100644 frappe/core/doctype/document_key/document_key.json create mode 100644 frappe/core/doctype/document_key/document_key.py create mode 100644 frappe/core/doctype/document_key/test_document_key.py diff --git a/frappe/core/doctype/communication/mixins.py b/frappe/core/doctype/communication/mixins.py index b6d8070d00..1d1d162728 100644 --- a/frappe/core/doctype/communication/mixins.py +++ b/frappe/core/doctype/communication/mixins.py @@ -149,7 +149,7 @@ class CommunicationEmailMixin: "doctype": self.reference_doctype, "name": self.reference_name, "print_format": print_format, - "key": get_parent_doc(self).get_signature() + "key": get_parent_doc(self).get_new_document_key() }) def get_outgoing_email_account(self): diff --git a/frappe/core/doctype/document_key/__init__.py b/frappe/core/doctype/document_key/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/frappe/core/doctype/document_key/document_key.js b/frappe/core/doctype/document_key/document_key.js new file mode 100644 index 0000000000..e8bda74ad2 --- /dev/null +++ b/frappe/core/doctype/document_key/document_key.js @@ -0,0 +1,8 @@ +// Copyright (c) 2021, Frappe Technologies and contributors +// For license information, please see license.txt + +frappe.ui.form.on('Document Key', { + // refresh: function(frm) { + + // } +}); diff --git a/frappe/core/doctype/document_key/document_key.json b/frappe/core/doctype/document_key/document_key.json new file mode 100644 index 0000000000..780432a730 --- /dev/null +++ b/frappe/core/doctype/document_key/document_key.json @@ -0,0 +1,65 @@ +{ + "actions": [], + "allow_rename": 1, + "autoname": "format:DOCUMENT-KEY-{########}", + "creation": "2021-12-20 03:34:58.744797", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "reference_doctype", + "reference_docname", + "key" + ], + "fields": [ + { + "fieldname": "reference_doctype", + "fieldtype": "Link", + "label": "Reference Document Type", + "options": "DocType", + "read_only": 1, + "search_index": 1 + }, + { + "fieldname": "reference_docname", + "fieldtype": "Dynamic Link", + "label": "Reference Document Name", + "options": "reference_doctype", + "read_only": 1, + "search_index": 1 + }, + { + "fieldname": "key", + "fieldtype": "Data", + "label": "Key", + "read_only": 1 + } + ], + "in_create": 1, + "index_web_pages_for_search": 1, + "links": [], + "modified": "2021-12-20 04:24:19.320505", + "modified_by": "Administrator", + "module": "Core", + "name": "Document Key", + "naming_rule": "Expression", + "owner": "Administrator", + "permissions": [ + { + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "System Manager", + "share": 1, + "write": 1 + } + ], + "read_only": 1, + "sort_field": "modified", + "sort_order": "DESC", + "states": [] +} \ No newline at end of file diff --git a/frappe/core/doctype/document_key/document_key.py b/frappe/core/doctype/document_key/document_key.py new file mode 100644 index 0000000000..3440545db6 --- /dev/null +++ b/frappe/core/doctype/document_key/document_key.py @@ -0,0 +1,8 @@ +# Copyright (c) 2021, Frappe Technologies and contributors +# For license information, please see license.txt + +# import frappe +from frappe.model.document import Document + +class DocumentKey(Document): + pass diff --git a/frappe/core/doctype/document_key/test_document_key.py b/frappe/core/doctype/document_key/test_document_key.py new file mode 100644 index 0000000000..5930ce72b4 --- /dev/null +++ b/frappe/core/doctype/document_key/test_document_key.py @@ -0,0 +1,8 @@ +# Copyright (c) 2021, Frappe Technologies and Contributors +# See license.txt + +# import frappe +import unittest + +class TestDocumentKey(unittest.TestCase): + pass diff --git a/frappe/model/document.py b/frappe/model/document.py index bbba9b1492..8cbb3d5975 100644 --- a/frappe/model/document.py +++ b/frappe/model/document.py @@ -1271,6 +1271,21 @@ class Document(BaseDocument): """Returns signature (hash) for private URL.""" return hashlib.sha224(get_datetime_str(self.creation).encode()).hexdigest() + def get_new_document_key(self): + doc = frappe.new_doc("Document Key") + doc.reference_doctype = self.doctype + doc.reference_docname = self.name + doc.key = frappe.generate_hash(length=32) + doc.insert(ignore_permissions=True) + return doc.key + + def is_document_key_valid(self, key): + return frappe.db.exists("Document Key", { + "reference_doctype": self.doctype, + "reference_docname": self.name, + "key": key + }) + def get_liked_by(self): liked_by = getattr(self, "_liked_by", None) if liked_by: diff --git a/frappe/www/printview.py b/frappe/www/printview.py index 569ebe27d6..549e69ff3b 100644 --- a/frappe/www/printview.py +++ b/frappe/www/printview.py @@ -227,7 +227,7 @@ def get_rendered_raw_commands(doc, name=None, print_format=None, meta=None, lang def validate_print_permission(doc): if frappe.form_dict.get("key"): - if frappe.form_dict.key == doc.get_signature(): + if doc.is_document_key_valid(frappe.form_dict.key): return for ptype in ("read", "print"): From 499625a85f7165f648a3e93c7e36d8015f43a027 Mon Sep 17 00:00:00 2001 From: Suraj Shetty Date: Mon, 27 Dec 2021 23:45:21 +0530 Subject: [PATCH 002/340] fix: Allow old signature as well --- frappe/www/printview.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/frappe/www/printview.py b/frappe/www/printview.py index 549e69ff3b..4251a448bc 100644 --- a/frappe/www/printview.py +++ b/frappe/www/printview.py @@ -230,6 +230,9 @@ def validate_print_permission(doc): if doc.is_document_key_valid(frappe.form_dict.key): return + if frappe.form_dict.key == doc.get_signature(): + return + for ptype in ("read", "print"): if (not frappe.has_permission(doc.doctype, ptype, doc) and not frappe.has_website_permission(doc)): From 64985a5d1a21911c93ea4d82798cd6481199fd59 Mon Sep 17 00:00:00 2001 From: Suraj Shetty Date: Tue, 28 Dec 2021 15:19:01 +0530 Subject: [PATCH 003/340] feat: Document key expiry --- frappe/core/doctype/doctype/doctype.json | 13 +++++++++- .../doctype/document_key/document_key.json | 25 +++++++++++++++++-- .../core/doctype/document_key/document_key.py | 16 ++++++++++-- .../customize_form/customize_form.json | 11 +++++++- frappe/exceptions.py | 1 + frappe/hooks.py | 1 + frappe/model/document.py | 6 +++-- 7 files changed, 65 insertions(+), 8 deletions(-) diff --git a/frappe/core/doctype/doctype/doctype.json b/frappe/core/doctype/doctype/doctype.json index 03e3b65ea1..9a777c740a 100644 --- a/frappe/core/doctype/doctype/doctype.json +++ b/frappe/core/doctype/doctype/doctype.json @@ -5,6 +5,7 @@ "creation": "2013-02-18 13:36:19", "description": "DocType is a Table / Form in the application.", "doctype": "DocType", + "document_key_expiry": 90, "document_type": "Document", "engine": "InnoDB", "field_order": [ @@ -56,6 +57,7 @@ "color", "show_preview_popup", "show_name_in_global_search", + "document_key_expiry", "email_settings_sb", "default_email_template", "column_break_51", @@ -582,6 +584,13 @@ "fieldname": "document_states_section", "fieldtype": "Section Break", "label": "Document States" + }, + { + "default": "90", + "description": "Set number of days after which a document key will be expired", + "fieldname": "document_key_expiry", + "fieldtype": "Int", + "label": "Document Key Expiry (in Days)" } ], "icon": "fa fa-bolt", @@ -663,10 +672,11 @@ "link_fieldname": "reference_doctype" } ], - "modified": "2021-12-09 14:53:10.717788", + "modified": "2021-12-28 03:19:19.033347", "modified_by": "Administrator", "module": "Core", "name": "DocType", + "naming_rule": "Set by user", "owner": "Administrator", "permissions": [ { @@ -696,5 +706,6 @@ "show_name_in_global_search": 1, "sort_field": "modified", "sort_order": "DESC", + "states": [], "track_changes": 1 } \ No newline at end of file diff --git a/frappe/core/doctype/document_key/document_key.json b/frappe/core/doctype/document_key/document_key.json index 780432a730..b9f26b2a7e 100644 --- a/frappe/core/doctype/document_key/document_key.json +++ b/frappe/core/doctype/document_key/document_key.json @@ -4,12 +4,15 @@ "autoname": "format:DOCUMENT-KEY-{########}", "creation": "2021-12-20 03:34:58.744797", "doctype": "DocType", + "document_key_expiry": 90, "editable_grid": 1, "engine": "InnoDB", "field_order": [ "reference_doctype", "reference_docname", - "key" + "key", + "expires_on", + "status" ], "fields": [ { @@ -33,12 +36,30 @@ "fieldtype": "Data", "label": "Key", "read_only": 1 + }, + { + "fieldname": "expires_on", + "fieldtype": "Date", + "in_list_view": 1, + "label": "Expires On", + "read_only": 1 + }, + { + "default": "Active", + "fieldname": "status", + "fieldtype": "Select", + "in_list_view": 1, + "in_standard_filter": 1, + "label": "Status", + "options": "Active\nExpired", + "read_only": 1, + "search_index": 1 } ], "in_create": 1, "index_web_pages_for_search": 1, "links": [], - "modified": "2021-12-20 04:24:19.320505", + "modified": "2021-12-28 00:43:57.811188", "modified_by": "Administrator", "module": "Core", "name": "Document Key", diff --git a/frappe/core/doctype/document_key/document_key.py b/frappe/core/doctype/document_key/document_key.py index 3440545db6..5b8fefcb80 100644 --- a/frappe/core/doctype/document_key/document_key.py +++ b/frappe/core/doctype/document_key/document_key.py @@ -1,8 +1,20 @@ # Copyright (c) 2021, Frappe Technologies and contributors # For license information, please see license.txt -# import frappe +import frappe from frappe.model.document import Document class DocumentKey(Document): - pass + def before_insert(self): + self.key = frappe.generate_hash(length=32) + if not self.expires_on: + meta = frappe.get_meta("DocType", self.reference_doctype) + self.expires_on = frappe.utils.add_days(None, days=meta.get("document_key_expiry") or 90) + + +def expire_document_keys(): + # called from hooks + frappe.db.set_value("Document Key", { + "expired_on": ["<", frappe.utils.nowdate()], + "status": "Active" + }, "status", "Expired") \ No newline at end of file diff --git a/frappe/custom/doctype/customize_form/customize_form.json b/frappe/custom/doctype/customize_form/customize_form.json index bdf95ad351..b1c67345d9 100644 --- a/frappe/custom/doctype/customize_form/customize_form.json +++ b/frappe/custom/doctype/customize_form/customize_form.json @@ -3,6 +3,7 @@ "autoname": "DL.####", "creation": "2013-01-29 17:55:08", "doctype": "DocType", + "document_key_expiry": 90, "document_type": "Document", "editable_grid": 1, "engine": "InnoDB", @@ -31,6 +32,7 @@ "default_print_format", "column_break_29", "show_preview_popup", + "document_key_expiry", "email_settings_section", "default_email_template", "column_break_26", @@ -296,6 +298,13 @@ "fieldtype": "Table", "label": "States", "options": "DocType State" + }, + { + "default": "90", + "description": "Set number of days after which a document key will be expired", + "fieldname": "document_key_expiry", + "fieldtype": "Int", + "label": "Document Key Expiry (in Days)" } ], "hide_toolbar": 1, @@ -304,7 +313,7 @@ "index_web_pages_for_search": 1, "issingle": 1, "links": [], - "modified": "2021-12-14 16:45:04.308690", + "modified": "2021-12-28 00:30:47.840699", "modified_by": "Administrator", "module": "Custom", "name": "Customize Form", diff --git a/frappe/exceptions.py b/frappe/exceptions.py index 8449425bc1..8993539603 100644 --- a/frappe/exceptions.py +++ b/frappe/exceptions.py @@ -82,6 +82,7 @@ class DocstatusTransitionError(ValidationError): pass class TimestampMismatchError(ValidationError): pass class EmptyTableError(ValidationError): pass class LinkExistsError(ValidationError): pass +class LinkExpiredError(ValidationError): pass class InvalidEmailAddressError(ValidationError): pass class InvalidNameError(ValidationError): pass class InvalidPhoneNumberError(ValidationError): pass diff --git a/frappe/hooks.py b/frappe/hooks.py index 4895c97200..d729e2ab55 100644 --- a/frappe/hooks.py +++ b/frappe/hooks.py @@ -241,6 +241,7 @@ scheduler_events = { "frappe.email.doctype.unhandled_email.unhandled_email.remove_old_unhandled_emails", "frappe.core.doctype.prepared_report.prepared_report.delete_expired_prepared_reports", "frappe.core.doctype.log_settings.log_settings.run_log_clean_up", + "frappe.core.doctype.document_key.document_key.expire_document_keys", "frappe.website.doctype.personal_data_deletion_request.personal_data_deletion_request.process_data_deletion_request" ], "daily_long": [ diff --git a/frappe/model/document.py b/frappe/model/document.py index 8cbb3d5975..ec83f5f7b0 100644 --- a/frappe/model/document.py +++ b/frappe/model/document.py @@ -1271,11 +1271,12 @@ class Document(BaseDocument): """Returns signature (hash) for private URL.""" return hashlib.sha224(get_datetime_str(self.creation).encode()).hexdigest() - def get_new_document_key(self): + @frappe.whitelist() + def get_new_document_key(self, expires_on=None): doc = frappe.new_doc("Document Key") doc.reference_doctype = self.doctype doc.reference_docname = self.name - doc.key = frappe.generate_hash(length=32) + doc.expires_on = expires_on doc.insert(ignore_permissions=True) return doc.key @@ -1283,6 +1284,7 @@ class Document(BaseDocument): return frappe.db.exists("Document Key", { "reference_doctype": self.doctype, "reference_docname": self.name, + "status": "Active", "key": key }) From 4ee99ce7e159018d654cf6f818e1d2c080430f3e Mon Sep 17 00:00:00 2001 From: Suraj Shetty Date: Thu, 13 Jan 2022 11:45:45 +0530 Subject: [PATCH 004/340] feat: Set expiry to document link --- frappe/model/document.py | 2 +- frappe/public/icons/timeless/symbol-defs.svg | 3 ++ frappe/public/js/frappe/form/controls/data.js | 14 ++++++- frappe/public/js/frappe/form/controls/date.js | 2 + frappe/public/js/frappe/form/form.js | 41 +++++++++++++++++++ frappe/public/js/frappe/form/toolbar.js | 7 ++++ frappe/public/scss/common/controls.scss | 26 ++++++++---- frappe/public/scss/common/css_variables.scss | 2 +- frappe/www/printview.html | 13 ++++-- frappe/www/printview.py | 11 +++-- 10 files changed, 104 insertions(+), 17 deletions(-) diff --git a/frappe/model/document.py b/frappe/model/document.py index ec83f5f7b0..633c6531b4 100644 --- a/frappe/model/document.py +++ b/frappe/model/document.py @@ -1286,7 +1286,7 @@ class Document(BaseDocument): "reference_docname": self.name, "status": "Active", "key": key - }) + }, cache=True) def get_liked_by(self): liked_by = getattr(self, "_liked_by", None) diff --git a/frappe/public/icons/timeless/symbol-defs.svg b/frappe/public/icons/timeless/symbol-defs.svg index b878f713e9..dcba913836 100644 --- a/frappe/public/icons/timeless/symbol-defs.svg +++ b/frappe/public/icons/timeless/symbol-defs.svg @@ -712,4 +712,7 @@ + + + diff --git a/frappe/public/js/frappe/form/controls/data.js b/frappe/public/js/frappe/form/controls/data.js index f4c9849528..2323ccee2f 100644 --- a/frappe/public/js/frappe/form/controls/data.js +++ b/frappe/public/js/frappe/form/controls/data.js @@ -58,7 +58,7 @@ frappe.ui.form.ControlData = class ControlData extends frappe.ui.form.ControlInp this.has_input = true; this.bind_change_event(); this.setup_autoname_check(); - + this.setup_copy_button(); if (this.df.options == 'URL') { this.setup_url_field(); } @@ -112,6 +112,18 @@ frappe.ui.form.ControlData = class ControlData extends frappe.ui.form.ControlInp }); } + setup_copy_button() { + if (this.df.with_copy_button) { + this.$wrapper.find('.control-input').append( + `` + ).find(".action-btn").click(() => { + frappe.utils.copy_to_clipboard(this.value); + }); + } + } + setup_barcode_field() { this.$wrapper.find('.control-input').append( ` diff --git a/frappe/public/js/frappe/form/controls/date.js b/frappe/public/js/frappe/form/controls/date.js index 28e7f2a478..5667fd05a4 100644 --- a/frappe/public/js/frappe/form/controls/date.js +++ b/frappe/public/js/frappe/form/controls/date.js @@ -62,6 +62,8 @@ frappe.ui.form.ControlDate = class ControlDate extends frappe.ui.form.ControlDat dateFormat: date_format, startDate: this.get_start_date(), keyboardNav: false, + minDate: this.df.min_date, + maxDate: this.df.max_date, onSelect: () => { this.$input.trigger('change'); }, diff --git a/frappe/public/js/frappe/form/form.js b/frappe/public/js/frappe/form/form.js index 8af1631b48..3ce7ff5777 100644 --- a/frappe/public/js/frappe/form/form.js +++ b/frappe/public/js/frappe/form/form.js @@ -1139,6 +1139,47 @@ frappe.ui.form.Form = class FrappeForm { }); } + share_doc_link() { + const share_document_url_dialog = new frappe.ui.Dialog({ + fields: [{ + fieldname: "visibility", + label: "Visibility", + fieldtype: "Select", + options: "Public\nPrivate", + default: "Public", + read_only_depends_on: "eval: doc.link" + }, { + fieldname: "link_expiration_date", + label: "Link Expiration Date", + fieldtype: "Date", + min_date: new Date(), + read_only_depends_on: "eval: doc.link" + }, { + fieldtype: "Button", + label: "Generate Link", + click: () => { + this.call("get_new_document_key").then(res => { + let key = res.message; + share_document_url_dialog.set_value("link", this.get_share_link(key)) + }) + } + }, { + fieldname: "link", + label: "Link", + fieldtype: "Data", + depends_on: "eval: doc.link", + with_copy_button: true + }], + size: "small", + title: __("Share {} Link", [this.doctype]), + }) + share_document_url_dialog.show() + } + + get_share_link(key) { + return `${window.origin}/${this.doctype}/${this.docname}?key=${key}`; + } + copy_doc(onload, from_amend) { this.validate_form_action("Create"); var newdoc = frappe.model.copy_doc(this.doc, from_amend); diff --git a/frappe/public/js/frappe/form/toolbar.js b/frappe/public/js/frappe/form/toolbar.js index ffcfc349ef..f5153eb67e 100644 --- a/frappe/public/js/frappe/form/toolbar.js +++ b/frappe/public/js/frappe/form/toolbar.js @@ -265,6 +265,13 @@ frappe.ui.form.Toolbar = class Toolbar { }); } + // share print view link + if (frappe.model.can_email(null, me.frm) && me.frm.doc.docstatus < 2) { + this.page.add_menu_item(__("Share Link", [me.frm.doctype]), function() { + me.frm.share_doc_link(); + }, true); + } + // go to field modal this.page.add_menu_item(__("Jump to field"), function() { me.show_jump_to_field_dialog(); diff --git a/frappe/public/scss/common/controls.scss b/frappe/public/scss/common/controls.scss index 954916c911..3cbc447d8f 100644 --- a/frappe/public/scss/common/controls.scss +++ b/frappe/public/scss/common/controls.scss @@ -288,13 +288,25 @@ textarea.form-control { position: relative; } -.link-btn { - position: absolute; - top: 4px; - right: 4px; - padding: 3px; - display: none; - z-index: 3; +.frappe-control { + .action-btn { + position: absolute; + top: 4px; + right: 4px; + padding: 3px; + z-index: 3; + } + + button.action-btn { + padding: 3px 5px; + background-color: var(--fg-color); + } + + .link-btn { + @extend .action-btn; + background-color: none; + display: none; + } } .phone-btn { diff --git a/frappe/public/scss/common/css_variables.scss b/frappe/public/scss/common/css_variables.scss index a14c19af2a..4293d6834b 100644 --- a/frappe/public/scss/common/css_variables.scss +++ b/frappe/public/scss/common/css_variables.scss @@ -219,7 +219,7 @@ --border-radius-full: 999px; --primary-color: #2490EF; - --btn-height: 28px; + --btn-height: 30px; // Checkbox --checkbox-right-margin: var(--margin-xs); diff --git a/frappe/www/printview.html b/frappe/www/printview.html index 3f8d4201fb..691c0cf3cf 100644 --- a/frappe/www/printview.html +++ b/frappe/www/printview.html @@ -11,11 +11,16 @@ -