From b112333f32eeb1f5652b5f7f7c1772a3b7033847 Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Wed, 20 Dec 2023 12:24:41 +0530 Subject: [PATCH 01/16] fix: ignore and gracefully handle img optimization failure PIL doesn't handle ALL image types. E.g. HEIC fails with bad error. (cherry picked from commit 3524cae48e00d9a9284f4550e2f3d9158cf4d838) # Conflicts: # frappe/utils/image.py --- frappe/utils/image.py | 39 +++++++++++++++++++++++++++------------ 1 file changed, 27 insertions(+), 12 deletions(-) diff --git a/frappe/utils/image.py b/frappe/utils/image.py index 11d9a86c4f8..3bf59bf659a 100644 --- a/frappe/utils/image.py +++ b/frappe/utils/image.py @@ -5,6 +5,8 @@ from PIL import Image +import frappe + def resize_images(path, maxdim=700): size = (maxdim, maxdim) @@ -51,19 +53,32 @@ def optimize_image( if content_type == "image/svg+xml": return content +<<<<<<< HEAD image = Image.open(io.BytesIO(content)) image_format = content_type.split("/")[1] size = max_width, max_height image.thumbnail(size, Image.Resampling.LANCZOS) - - output = io.BytesIO() - image.save( - output, - format=image_format, - optimize=optimize, - quality=quality, - save_all=True if image_format == "gif" else None, - ) - - optimized_content = output.getvalue() - return optimized_content if len(optimized_content) < len(content) else content +======= + try: + image = Image.open(io.BytesIO(content)) + width, height = image.size + max_height = max(min(max_height, height * 0.8), 200) + max_width = max(min(max_width, width * 0.8), 200) + image_format = content_type.split("/")[1] + size = max_width, max_height + image.thumbnail(size, Image.Resampling.LANCZOS) +>>>>>>> 3524cae48e (fix: ignore and gracefully handle img optimization failure) + + output = io.BytesIO() + image.save( + output, + format=image_format, + optimize=optimize, + quality=quality, + save_all=True if image_format == "gif" else None, + ) + optimized_content = output.getvalue() + return optimized_content if len(optimized_content) < len(content) else content + except Exception as e: + frappe.msgprint(frappe._("Failed to optimize image: {0}").format(str(e))) + return content From b8c9eb62877663be7df29515221539a10759cb78 Mon Sep 17 00:00:00 2001 From: Akhil Narang Date: Wed, 14 Feb 2024 18:27:07 +0530 Subject: [PATCH 02/16] chore: resolve conflicts Signed-off-by: Akhil Narang --- frappe/utils/image.py | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/frappe/utils/image.py b/frappe/utils/image.py index 3bf59bf659a..6150743e33b 100644 --- a/frappe/utils/image.py +++ b/frappe/utils/image.py @@ -53,21 +53,11 @@ def optimize_image( if content_type == "image/svg+xml": return content -<<<<<<< HEAD - image = Image.open(io.BytesIO(content)) - image_format = content_type.split("/")[1] - size = max_width, max_height - image.thumbnail(size, Image.Resampling.LANCZOS) -======= try: image = Image.open(io.BytesIO(content)) - width, height = image.size - max_height = max(min(max_height, height * 0.8), 200) - max_width = max(min(max_width, width * 0.8), 200) image_format = content_type.split("/")[1] size = max_width, max_height image.thumbnail(size, Image.Resampling.LANCZOS) ->>>>>>> 3524cae48e (fix: ignore and gracefully handle img optimization failure) output = io.BytesIO() image.save( From 136ac70d19b7187fed88eaac04665753ba4f010e Mon Sep 17 00:00:00 2001 From: barredterra <14891507+barredterra@users.noreply.github.com> Date: Fri, 16 Feb 2024 20:43:37 +0100 Subject: [PATCH 03/16] fix: invite contact as user (cherry picked from commit bbd42839e387b59304d7d376e136de6a0b86ca9e) --- frappe/contacts/doctype/contact/contact.js | 7 +++++- frappe/contacts/doctype/contact/contact.py | 28 +++++++++++----------- 2 files changed, 20 insertions(+), 15 deletions(-) diff --git a/frappe/contacts/doctype/contact/contact.js b/frappe/contacts/doctype/contact/contact.js index d4ae9379fa2..f1a40e106fc 100644 --- a/frappe/contacts/doctype/contact/contact.js +++ b/frappe/contacts/doctype/contact/contact.js @@ -21,7 +21,12 @@ frappe.ui.form.on("Contact", { } } - if (!frm.doc.user && !frm.is_new() && frm.perm[0].write) { + if ( + !frm.doc.user && + !frm.is_new() && + frm.perm[0].write && + frappe.boot.user.can_create.includes("User") + ) { frm.add_custom_button(__("Invite as User"), function () { return frappe.call({ method: "frappe.contacts.doctype.contact.contact.invite_user", diff --git a/frappe/contacts/doctype/contact/contact.py b/frappe/contacts/doctype/contact/contact.py index 1ed6b8ac912..d3130e0fe72 100644 --- a/frappe/contacts/doctype/contact/contact.py +++ b/frappe/contacts/doctype/contact/contact.py @@ -153,25 +153,25 @@ def get_default_contact(doctype, name): @frappe.whitelist() -def invite_user(contact): +def invite_user(contact: str): contact = frappe.get_doc("Contact", contact) + contact.check_permission() if not contact.email_id: frappe.throw(_("Please set Email Address")) - if contact.has_permission("write"): - user = frappe.get_doc( - { - "doctype": "User", - "first_name": contact.first_name, - "last_name": contact.last_name, - "email": contact.email_id, - "user_type": "Website User", - "send_welcome_email": 1, - } - ).insert(ignore_permissions=True) - - return user.name + user = frappe.get_doc( + { + "doctype": "User", + "first_name": contact.first_name, + "last_name": contact.last_name, + "email": contact.email_id, + "user_type": "Website User", + "send_welcome_email": 1, + } + ).insert() + + return user.name @frappe.whitelist() From 427e68190dd1c0be8da029aacbd535e4de42d94b Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Mon, 19 Feb 2024 05:32:54 +0000 Subject: [PATCH 04/16] fix: lower socket timeout for validating email domain (#24915) (#24917) (cherry picked from commit 59f8e361a575e6bd883512f883f5e953ca5afbfa) # Conflicts: # frappe/email/doctype/email_domain/email_domain.py Co-authored-by: Ankush Menat --- frappe/email/doctype/email_domain/email_domain.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/frappe/email/doctype/email_domain/email_domain.py b/frappe/email/doctype/email_domain/email_domain.py index 528407916ac..3b6c79bd795 100644 --- a/frappe/email/doctype/email_domain/email_domain.py +++ b/frappe/email/doctype/email_domain/email_domain.py @@ -85,7 +85,7 @@ def validate_incoming_server_conn(self): conn_method = Timed_POP3_SSL if self.use_ssl else Timed_POP3 self.use_starttls = cint(self.use_imap and self.use_starttls and not self.use_ssl) - incoming_conn = conn_method(self.email_server, port=self.incoming_port) + incoming_conn = conn_method(self.email_server, port=self.incoming_port, timeout=30) incoming_conn.logout() if self.use_imap else incoming_conn.quit() @handle_error("outgoing") @@ -98,4 +98,4 @@ def validate_outgoing_server_conn(self): elif self.use_tls: self.smtp_port = self.smtp_port or 587 - conn_method((self.smtp_server or ""), cint(self.smtp_port) or 0).quit() + conn_method((self.smtp_server or ""), cint(self.smtp_port), timeout=30).quit() From 17156384c44b6a9db61f08480e40f94f6ebb5a14 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Mon, 19 Feb 2024 11:19:42 +0530 Subject: [PATCH 05/16] fix(weblist): Fix "More" button not working (#24893) (#24925) (cherry picked from commit 02051fcf77c2dae08e3c094d0fe54129dd6319e8) Co-authored-by: Corentin Flr <10946971+cogk@users.noreply.github.com> --- frappe/templates/includes/list/list.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frappe/templates/includes/list/list.js b/frappe/templates/includes/list/list.js index 27abe7c393b..5580c04f9e3 100644 --- a/frappe/templates/includes/list/list.js +++ b/frappe/templates/includes/list/list.js @@ -6,7 +6,7 @@ frappe.ready(function() { var btn = $(this); var data = $.extend(frappe.utils.get_query_params(), { doctype: "{{ doctype }}", - txt: "{{ txt|e or '' }}", + txt: "{{ (txt or '')|e }}", limit_start: next_start, pathname: location.pathname, }); From fa184d65b4c9cb5c7c2e4c7a38b5c44ec34cc1de Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Mon, 19 Feb 2024 05:51:07 +0000 Subject: [PATCH 06/16] fix(calendar): Replace route in load_last_view (#24894) (#24927) (cherry picked from commit 8a27e7632ddecfdc262f6c6f5daaf0b6619df180) Co-authored-by: Corentin Flr <10946971+cogk@users.noreply.github.com> --- frappe/public/js/frappe/views/calendar/calendar.js | 1 + 1 file changed, 1 insertion(+) diff --git a/frappe/public/js/frappe/views/calendar/calendar.js b/frappe/public/js/frappe/views/calendar/calendar.js index 2bfbf625381..8c240b4b458 100644 --- a/frappe/public/js/frappe/views/calendar/calendar.js +++ b/frappe/public/js/frappe/views/calendar/calendar.js @@ -11,6 +11,7 @@ frappe.views.CalendarView = class CalendarView extends frappe.views.ListView { const doctype = route[1]; const user_settings = frappe.get_user_settings(doctype)["Calendar"] || {}; route.push(user_settings.last_calendar || "default"); + frappe.route_flags.replace_route = true; frappe.set_route(route); return true; } else { From 3347144f040171fd1bf4adddd641635d4f704edf Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Mon, 19 Feb 2024 05:53:29 +0000 Subject: [PATCH 07/16] fix: keep order in `get_values_from_single` (#24907) (#24923) * fix: keep order in `get_values_from_single` * fix: add test for destructuring --------- Co-authored-by: Ankush Menat (cherry picked from commit 68eb2d978db6fe2f9707a4094a054659ce9d85ed) Co-authored-by: Raffael Meyer <14891507+barredterra@users.noreply.github.com> --- frappe/database/database.py | 22 ++++++++++++---------- frappe/tests/test_db.py | 7 +++++++ 2 files changed, 19 insertions(+), 10 deletions(-) diff --git a/frappe/database/database.py b/frappe/database/database.py index 89ac69a4d34..a7a7423024e 100644 --- a/frappe/database/database.py +++ b/frappe/database/database.py @@ -732,16 +732,18 @@ def get_values_from_single( if not run: return r - if as_dict: - if r: - r = frappe._dict(r) - if update: - r.update(update) - return [r] - else: - return [] - else: - return r and [[i[1] for i in r]] or [] + + if not r: + return [] + + r = frappe._dict(r) + if update: + r.update(update) + + if not as_dict: + return [[r.get(field) for field in fields]] + + return [r] def get_singles_dict(self, doctype, debug=False, *, for_update=False, cast=False): """Get Single DocType as dict. diff --git a/frappe/tests/test_db.py b/frappe/tests/test_db.py index 3ab879d2cc9..a94c52e3280 100644 --- a/frappe/tests/test_db.py +++ b/frappe/tests/test_db.py @@ -173,6 +173,13 @@ def test_get_single_value(self): # teardown clear_custom_fields("Print Settings") + def test_get_single_value_destructuring(self): + [[lang, date_format]] = frappe.db.get_values_from_single( + ["language", "date_format"], None, "System Settings" + ) + self.assertEqual(lang, frappe.db.get_single_value("System Settings", "language")) + self.assertEqual(date_format, frappe.db.get_single_value("System Settings", "date_format")) + def test_log_touched_tables(self): frappe.flags.in_migrate = True frappe.flags.touched_tables = set() From 8087a7a58a011425029cfb4ca32c47a71d32d1a3 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Mon, 19 Feb 2024 11:30:50 +0530 Subject: [PATCH 08/16] fix: ensure has_value_changed works for Datetime, Date and Time fields (backport #24919) (#24921) * fix: ensure has_value_changed works for datetime, date and timedelta fields (cherry picked from commit a1cb19c820f0881b47b1e11503b39044cc6b4162) # Conflicts: # frappe/model/document.py # frappe/utils/data.py * test: add more tests for has_value_changed (cherry picked from commit 0d847439b6ac5deced930d15bc3ba5d349c1c739) # Conflicts: # frappe/model/document.py * chore: conflicts --------- Co-authored-by: scdanieli <23150094+scdanieli@users.noreply.github.com> Co-authored-by: Ankush Menat --- frappe/model/document.py | 22 +++++++++++++++++++--- frappe/tests/test_document.py | 7 ++++++- frappe/tests/test_utils.py | 1 + frappe/utils/data.py | 12 ++++++++---- 4 files changed, 34 insertions(+), 8 deletions(-) diff --git a/frappe/model/document.py b/frappe/model/document.py index 3b190c50cbd..82fe8d55087 100644 --- a/frappe/model/document.py +++ b/frappe/model/document.py @@ -19,7 +19,7 @@ from frappe.model.utils import is_virtual_doctype from frappe.model.workflow import set_workflow_state_on_action, validate_workflow from frappe.utils import cstr, date_diff, file_lock, flt, get_datetime_str, now -from frappe.utils.data import get_absolute_url +from frappe.utils.data import get_absolute_url, get_datetime, get_timedelta, getdate from frappe.utils.global_search import update_global_search @@ -422,9 +422,25 @@ def get_doc_before_save(self): return getattr(self, "_doc_before_save", None) def has_value_changed(self, fieldname): - """Returns true if value is changed before and after saving""" + """Return True if value has changed before and after saving.""" + from datetime import date, datetime, timedelta + previous = self.get_doc_before_save() - return previous.get(fieldname) != self.get(fieldname) if previous else True + + if not previous: + return True + + previous_value = previous.get(fieldname) + current_value = self.get(fieldname) + + if isinstance(previous_value, datetime): + current_value = get_datetime(current_value) + elif isinstance(previous_value, date): + current_value = getdate(current_value) + elif isinstance(previous_value, timedelta): + current_value = get_timedelta(current_value) + + return previous_value != current_value def set_new_name(self, force=False, set_name=None, set_child_names=True): """Calls `frappe.naming.set_new_name` for parent and child docs.""" diff --git a/frappe/tests/test_document.py b/frappe/tests/test_document.py index 3079f85cc94..3ebbabee1da 100644 --- a/frappe/tests/test_document.py +++ b/frappe/tests/test_document.py @@ -101,8 +101,13 @@ def test_update(self): def test_value_changed(self): d = self.test_insert() d.subject = "subject changed again" - d.save() + d.load_doc_before_save() + d.update_modified() + self.assertTrue(d.has_value_changed("subject")) + self.assertTrue(d.has_value_changed("modified")) + + self.assertFalse(d.has_value_changed("creation")) self.assertFalse(d.has_value_changed("event_type")) def test_mandatory(self): diff --git a/frappe/tests/test_utils.py b/frappe/tests/test_utils.py index 25058df5760..0c852299468 100644 --- a/frappe/tests/test_utils.py +++ b/frappe/tests/test_utils.py @@ -482,6 +482,7 @@ def test_get_timedelta(self): self.assertIsInstance(get_timedelta(str(datetime_input)), timedelta) self.assertIsInstance(get_timedelta(str(timedelta_input)), timedelta) self.assertIsInstance(get_timedelta(str(time_input)), timedelta) + self.assertIsInstance(get_timedelta(get_timedelta("100:2:12")), timedelta) def test_date_from_timegrain(self): start_date = getdate("2021-01-01") diff --git a/frappe/utils/data.py b/frappe/utils/data.py index 7ab308951de..e5e19ca7c22 100644 --- a/frappe/utils/data.py +++ b/frappe/utils/data.py @@ -127,12 +127,13 @@ def get_datetime( return parser.parse(datetime_str) -def get_timedelta(time: str | None = None) -> datetime.timedelta | None: - """Return `datetime.timedelta` object from string value of a - valid time format. Returns None if `time` is not a valid format +def get_timedelta(time: str | datetime.timedelta | None = None) -> datetime.timedelta | None: + """Return `datetime.timedelta` object from string value of a valid time format. + + Return None if `time` is not a valid format. Args: - time (str): A valid time representation. This string is parsed + time (str | datetime.timedelta): A valid time representation. This string is parsed using `dateutil.parser.parse`. Examples of valid inputs are: '0:0:0', '17:21:00', '2012-01-19 17:21:00'. Checkout https://dateutil.readthedocs.io/en/stable/parser.html#dateutil.parser.parse @@ -140,6 +141,9 @@ def get_timedelta(time: str | None = None) -> datetime.timedelta | None: Returns: datetime.timedelta: Timedelta object equivalent of the passed `time` string """ + if isinstance(time, datetime.timedelta): + return time + time = time or "0:0:0" try: From 3a36f7d743345fd05c2820d021855e5d4d694afc Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Mon, 19 Feb 2024 12:06:34 +0530 Subject: [PATCH 09/16] fix(page): Catch LocalStorage quota exception (#24885) (#24930) (cherry picked from commit fe8016928715e43317d677668f71006aa7bc4ffd) Co-authored-by: Corentin Flr <10946971+cogk@users.noreply.github.com> --- frappe/public/js/frappe/views/pageview.js | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/frappe/public/js/frappe/views/pageview.js b/frappe/public/js/frappe/views/pageview.js index d9d9f936d08..67817ca7c15 100644 --- a/frappe/public/js/frappe/views/pageview.js +++ b/frappe/public/js/frappe/views/pageview.js @@ -31,7 +31,11 @@ frappe.views.pageview = { args: { name: name }, callback: function (r) { if (!r.docs._dynamic_page) { - localStorage["_page:" + name] = JSON.stringify(r.docs); + try { + localStorage["_page:" + name] = JSON.stringify(r.docs); + } catch (e) { + console.warn(e); + } } callback(); }, From 5f29c836c6463805a4b0458772286a642cd68013 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Mon, 19 Feb 2024 06:55:24 +0000 Subject: [PATCH 10/16] fix: send_workflow_action_email (#24929) (#24933) * fix: send_workflow_action_email * fix: send_workflow_action_email (cherry picked from commit 15eb1bf8395e3f5f3a71e30ba3efda3a1b7010e9) Co-authored-by: Nihantra C. Patel <141945075+Nihantra-Patel@users.noreply.github.com> --- .../workflow/doctype/workflow_action/workflow_action.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/frappe/workflow/doctype/workflow_action/workflow_action.py b/frappe/workflow/doctype/workflow_action/workflow_action.py index f50c8bf6c74..5a3ea508397 100644 --- a/frappe/workflow/doctype/workflow_action/workflow_action.py +++ b/frappe/workflow/doctype/workflow_action/workflow_action.py @@ -308,7 +308,7 @@ def get_users_next_action_data(transitions, doc): def user_has_permission(user: str) -> bool: from frappe.permissions import has_permission - return has_permission(doctype=doc, user=user, print_logs=False) + return has_permission(doctype=doc, user=user) for transition in transitions: users = get_users_with_role(transition.allowed) @@ -359,10 +359,10 @@ def send_workflow_action_email(doc, transitions): users_data = get_users_next_action_data(transitions, doc) common_args = get_common_email_args(doc) message = common_args.pop("message", None) - for d in users_data: + for user, data in users_data.items(): # noqa: B007 email_args = { - "recipients": [d.get("email")], - "args": {"actions": list(deduplicate_actions(d.get("possible_actions"))), "message": message}, + "recipients": [data.get("email")], + "args": {"actions": list(deduplicate_actions(data.get("possible_actions"))), "message": message}, "reference_name": doc.name, "reference_doctype": doc.doctype, } From 0632cb397252b37912ba71d37488e7e60367f281 Mon Sep 17 00:00:00 2001 From: barredterra <14891507+barredterra@users.noreply.github.com> Date: Tue, 13 Feb 2024 15:36:53 +0100 Subject: [PATCH 11/16] fix: use communication date in timeline Before, we used the database creation date, which coincidentally corresponds to the real communication date in many cases. (cherry picked from commit 10bd9a7efd7389ae0457ff53cc90e5a1044cc5cd) # Conflicts: # frappe/desk/form/load.py --- frappe/desk/form/load.py | 8 +++++++- frappe/public/js/frappe/form/footer/form_timeline.js | 2 +- .../js/frappe/form/templates/timeline_message_box.html | 6 +++--- 3 files changed, 11 insertions(+), 5 deletions(-) diff --git a/frappe/desk/form/load.py b/frappe/desk/form/load.py index f4dd2e0836c..b9703f8737f 100644 --- a/frappe/desk/form/load.py +++ b/frappe/desk/form/load.py @@ -290,9 +290,15 @@ def get_communication_data( conditions = "" if after: # find after a particular date +<<<<<<< HEAD conditions += """ AND C.creation > {} """.format(after) +======= + conditions += f""" + AND C.communication_date > {after} + """ +>>>>>>> 10bd9a7efd (fix: use communication date in timeline) if doctype == "User": conditions += """ @@ -323,7 +329,7 @@ def get_communication_data( SELECT * FROM (({part1}) UNION ({part2})) AS combined {group_by} - ORDER BY creation DESC + ORDER BY communication_date DESC LIMIT %(limit)s OFFSET %(start)s """.format(part1=part1, part2=part2, group_by=(group_by or "")), diff --git a/frappe/public/js/frappe/form/footer/form_timeline.js b/frappe/public/js/frappe/form/footer/form_timeline.js index 13fb2f87fdf..c028f0e52bd 100644 --- a/frappe/public/js/frappe/form/footer/form_timeline.js +++ b/frappe/public/js/frappe/form/footer/form_timeline.js @@ -243,7 +243,7 @@ class FormTimeline extends BaseTimeline { communication_timeline_contents.push({ icon: icon_set[medium], icon_size: "sm", - creation: communication.creation, + creation: communication.communication_date, is_card: true, content: this.get_communication_timeline_content(communication), doctype: "Communication", diff --git a/frappe/public/js/frappe/form/templates/timeline_message_box.html b/frappe/public/js/frappe/form/templates/timeline_message_box.html index 4d91ec117c0..7392a6a97f2 100644 --- a/frappe/public/js/frappe/form/templates/timeline_message_box.html +++ b/frappe/public/js/frappe/form/templates/timeline_message_box.html @@ -23,7 +23,7 @@ {% } %}
- {{ comment_when(doc.creation) }} + {{ comment_when(doc.communication_date) }}
{% } else if (doc.comment_type && doc.comment_type == "Comment") { %} @@ -33,7 +33,7 @@ {{ __("commented") }} . - {{ comment_when(doc.creation) }} + {{ comment_when(doc.communication_date) }} {% } else { %} @@ -44,7 +44,7 @@ {{ doc.user_full_name || frappe.user.full_name(doc.owner) }} . - {{ comment_when(doc.creation) }} + {{ comment_when(doc.communication_date) }} {% if (doc.subject) { %}
{{doc.subject}}
From 4efcdb506a7f9243c702ec1aedf3ea0dd83f3421 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Mon, 19 Feb 2024 10:20:17 +0000 Subject: [PATCH 12/16] fix: handle bad cron expressions (backport #24938) (#24941) * fix: delete cron jobs when switching server script type (cherry picked from commit 68d1a9ec0767bc31599bb6df009efdaf81e56c40) * fix: validate cron format (cherry picked from commit e88c078c9d547b421a9f9317325c13858f10ecf9) # Conflicts: # frappe/core/doctype/scheduled_job_type/scheduled_job_type.py --------- Co-authored-by: Ankush Menat --- .../scheduled_job_type/scheduled_job_type.py | 16 ++++++++++++++-- .../core/doctype/server_script/server_script.py | 9 +++++---- 2 files changed, 19 insertions(+), 6 deletions(-) diff --git a/frappe/core/doctype/scheduled_job_type/scheduled_job_type.py b/frappe/core/doctype/scheduled_job_type/scheduled_job_type.py index 436ffad9874..51acba84a8c 100644 --- a/frappe/core/doctype/scheduled_job_type/scheduled_job_type.py +++ b/frappe/core/doctype/scheduled_job_type/scheduled_job_type.py @@ -5,9 +5,10 @@ from datetime import datetime import click -from croniter import croniter +from croniter import CroniterBadCronError, croniter import frappe +from frappe import _ from frappe.model.document import Document from frappe.utils import get_datetime, now_datetime from frappe.utils.background_jobs import enqueue, is_job_enqueued @@ -22,7 +23,18 @@ def validate(self): # force logging for all events other than continuous ones (ALL) self.create_log = 1 - def enqueue(self, force=False): + if self.frequency == "Cron": + if not self.cron_format: + frappe.throw(_("Cron format is required for job types with Cron frequency.")) + try: + croniter(self.cron_format) + except CroniterBadCronError: + frappe.throw( + _("{0} is not a valid Cron expression.").format(f"{self.cron_format}"), + title=_("Bad Cron Expression"), + ) + + def enqueue(self, force=False) -> bool: # enqueue event if last execution is done if self.is_event_due() or force: if frappe.flags.enqueued_jobs: diff --git a/frappe/core/doctype/server_script/server_script.py b/frappe/core/doctype/server_script/server_script.py index 35a32130fd8..cc050f8512d 100644 --- a/frappe/core/doctype/server_script/server_script.py +++ b/frappe/core/doctype/server_script/server_script.py @@ -60,11 +60,12 @@ def sync_scheduler_events(self): def clear_scheduled_events(self): """Deletes existing scheduled jobs by Server Script if self.event_frequency or self.cron_format has changed""" - if self.script_type == "Scheduler Event" and ( - self.has_value_changed("event_frequency") or self.has_value_changed("cron_format") - ): + if ( + self.script_type == "Scheduler Event" + and (self.has_value_changed("event_frequency") or self.has_value_changed("cron_format")) + ) or (self.has_value_changed("script_type") and self.script_type != "Scheduler Event"): for scheduled_job in self.scheduled_jobs: - frappe.delete_doc("Scheduled Job Type", scheduled_job.name) + frappe.delete_doc("Scheduled Job Type", scheduled_job.name, delete_permanently=1) def check_if_compilable_in_restricted_context(self): """Check compilation errors and send them back as warnings.""" From f0352cee3b0f711e67d3b3507e67abd5141d2f7d Mon Sep 17 00:00:00 2001 From: Raffael Meyer <14891507+barredterra@users.noreply.github.com> Date: Mon, 19 Feb 2024 12:45:31 +0100 Subject: [PATCH 13/16] chore: resolve conflicts --- frappe/desk/form/load.py | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/frappe/desk/form/load.py b/frappe/desk/form/load.py index b9703f8737f..7ca1132e180 100644 --- a/frappe/desk/form/load.py +++ b/frappe/desk/form/load.py @@ -290,15 +290,9 @@ def get_communication_data( conditions = "" if after: # find after a particular date -<<<<<<< HEAD conditions += """ - AND C.creation > {} + AND C.communication_date > {} """.format(after) -======= - conditions += f""" - AND C.communication_date > {after} - """ ->>>>>>> 10bd9a7efd (fix: use communication date in timeline) if doctype == "User": conditions += """ From 64ac07aee467bc38b306c1c79954b41c4998e5b4 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Mon, 19 Feb 2024 13:23:59 +0000 Subject: [PATCH 14/16] refactor: Reset password flow (backport #24857) (#24860) * chore: remove default UI tours Experiment :shrug: (cherry picked from commit 74071c123113913b9637cab259c581138307bc8a) * refactor: Reset password flow - Hash one time reset tokens instead of storing them as is - Up the perm level - Use better source of randomness for generating token - minor code cleanup here and there (cherry picked from commit 4c925e03253985266bc7a2b1cddc707aaa1f3df3) # Conflicts: # frappe/core/doctype/user/user.json # frappe/core/doctype/user/user.py # frappe/patches.txt # frappe/tests/test_utils.py * test: redo reset password tests (cherry picked from commit 38565a80e3985c805eaf677db3018a421edd2342) # Conflicts: # frappe/core/doctype/user/test_user.py # frappe/tests/utils.py * fix(UX): usability of reset password page - password managers use paste, added that event to trigger our code - if no password policy then skip. Way too much needless business logic in HTML here. (cherry picked from commit 78264a7f168e1ec2e3703a64a12745963d129b08) * chore: conflicts --------- Co-authored-by: Ankush Menat --- frappe/core/doctype/user/test_user.py | 38 ++++---- frappe/core/doctype/user/user.json | 18 ++-- frappe/core/doctype/user/user.py | 20 ++-- .../user_list_tour/user_list_tour.json | 95 ------------------- .../main_workspace_tour.json | 79 --------------- .../users_workspace_tour.json | 62 ------------ frappe/tests/test_utils.py | 10 ++ frappe/tests/utils.py | 8 +- frappe/utils/data.py | 8 ++ frappe/www/update-password.html | 13 ++- 10 files changed, 75 insertions(+), 276 deletions(-) delete mode 100644 frappe/core/form_tour/user_list_tour/user_list_tour.json delete mode 100644 frappe/desk/form_tour/main_workspace_tour/main_workspace_tour.json delete mode 100644 frappe/desk/form_tour/users_workspace_tour/users_workspace_tour.json diff --git a/frappe/core/doctype/user/test_user.py b/frappe/core/doctype/user/test_user.py index ed86ba475a1..b6658bf2f15 100644 --- a/frappe/core/doctype/user/test_user.py +++ b/frappe/core/doctype/user/test_user.py @@ -3,6 +3,7 @@ import json import time from unittest.mock import patch +from urllib.parse import parse_qs, urlparse import frappe import frappe.exceptions @@ -17,7 +18,7 @@ from frappe.desk.notifications import extract_mentions from frappe.frappeclient import FrappeClient from frappe.model.delete_doc import delete_doc -from frappe.tests.utils import FrappeTestCase +from frappe.tests.utils import FrappeTestCase, change_settings from frappe.utils import get_url user_module = frappe.core.doctype.user.user @@ -32,10 +33,14 @@ def tearDown(self): frappe.db.set_single_value("System Settings", "password_reset_limit", 3) frappe.set_user("Administrator") + @staticmethod + def reset_password(user) -> str: + link = user.reset_password() + return parse_qs(urlparse(link).query)["key"][0] + def test_user_type(self): - new_user = frappe.get_doc( - dict(doctype="User", email="test-for-type@example.com", first_name="Tester") - ).insert(ignore_if_duplicate=True) + user_id = frappe.generate_hash() + "@example.com" + new_user = frappe.get_doc(doctype="User", email=user_id, first_name="Tester").insert() self.assertEqual(new_user.user_type, "Website User") # social login userid for frappe @@ -269,11 +274,8 @@ def test_comment_mentions(self): """ self.assertListEqual(extract_mentions(comment), ["test@example.com", "test1@example.com"]) + @change_settings("System Settings", commit=True, password_reset_limit=1) def test_rate_limiting_for_reset_password(self): - # Allow only one reset request for a day - frappe.db.set_single_value("System Settings", "password_reset_limit", 1) - frappe.db.commit() - url = get_url() data = {"cmd": "frappe.core.doctype.user.user.reset_password", "user": "test@test.com"} @@ -351,6 +353,7 @@ def test_signup(self): "/signup", ) + @change_settings("System Settings", password_reset_limit=6) def test_reset_password(self): from frappe.auth import CookieManager, LoginManager from frappe.utils import set_request @@ -363,12 +366,11 @@ def test_reset_password(self): frappe.local.login_manager = LoginManager() # used by rate limiter when calling reset_password frappe.local.request_ip = "127.0.0.69" - frappe.db.set_single_value("System Settings", "password_reset_limit", 6) frappe.set_user("testpassword@example.com") test_user = frappe.get_doc("User", "testpassword@example.com") - test_user.reset_password() - self.assertEqual(update_password(new_password, key=test_user.reset_password_key), "/app") + key = self.reset_password(test_user) + self.assertEqual(update_password(new_password, key=key), "/app") self.assertEqual( update_password(new_password, key="wrong_key"), "The reset password link has either been used before or is invalid", @@ -411,7 +413,9 @@ def test_reset_password(self): test_user = frappe.get_doc("User", "test2@example.com") self.assertEqual(reset_password(user="test2@example.com"), None) test_user.reload() - self.assertEqual(update_password(new_password, key=test_user.reset_password_key), "/") + link = sendmail.call_args_list[0].kwargs["args"]["link"] + key = parse_qs(urlparse(link).query)["key"][0] + self.assertEqual(update_password(new_password, key=key), "/") update_password(old_password, old_password=new_password) self.assertEqual( json.loads(frappe.message_log[0]).get("message"), @@ -437,16 +441,16 @@ def test_user_onload_modules(self): sorted(m.get("module_name") for m in get_modules_from_all_apps()), ) + @change_settings("System Settings", reset_password_link_expiry_duration=1) def test_reset_password_link_expiry(self): new_password = "new_password" - # set the reset password expiry to 1 second - frappe.db.set_single_value("System Settings", "reset_password_link_expiry_duration", 1) frappe.set_user("testpassword@example.com") test_user = frappe.get_doc("User", "testpassword@example.com") - test_user.reset_password() - time.sleep(1) # sleep for 1 sec to expire the reset link + key = self.reset_password(test_user) + time.sleep(1) + self.assertEqual( - update_password(new_password, key=test_user.reset_password_key), + update_password(new_password, key=key), "The reset password link has been expired", ) diff --git a/frappe/core/doctype/user/user.json b/frappe/core/doctype/user/user.json index 3461f50dd50..54c99552ea5 100644 --- a/frappe/core/doctype/user/user.json +++ b/frappe/core/doctype/user/user.json @@ -323,6 +323,7 @@ "hidden": 1, "label": "Reset Password Key", "no_copy": 1, + "permlevel": 1, "print_hide": 1, "read_only": 1 }, @@ -617,13 +618,14 @@ "options": "Module Profile" }, { - "description": "Stores the datetime when the last reset password key was generated.", - "fieldname": "last_reset_password_key_generated_on", - "fieldtype": "Datetime", - "hidden": 1, - "label": "Last Reset Password Key Generated On", - "read_only": 1 - }, + "description": "Stores the datetime when the last reset password key was generated.", + "fieldname": "last_reset_password_key_generated_on", + "fieldtype": "Datetime", + "hidden": 1, + "label": "Last Reset Password Key Generated On", + "permlevel": 1, + "read_only": 1 + }, { "fieldname": "column_break_75", "fieldtype": "Column Break" @@ -731,7 +733,7 @@ "link_fieldname": "user" } ], - "modified": "2023-06-05 17:26:04.127555", + "modified": "2024-02-11 13:16:29.574666", "modified_by": "Administrator", "module": "Core", "name": "User", diff --git a/frappe/core/doctype/user/user.py b/frappe/core/doctype/user/user.py index ce97b7436d7..e5bf13dbf0e 100644 --- a/frappe/core/doctype/user/user.py +++ b/frappe/core/doctype/user/user.py @@ -28,6 +28,7 @@ now_datetime, today, ) +from frappe.utils.data import sha256_hash from frappe.utils.deprecations import deprecated from frappe.utils.password import check_password, get_password_reset_limit from frappe.utils.password import update_password as _update_password @@ -297,10 +298,11 @@ def validate_reset_password(self): pass def reset_password(self, send_email=False, password_expired=False): - from frappe.utils import get_url, random_string + from frappe.utils import get_url - key = random_string(32) - self.db_set("reset_password_key", key) + key = frappe.generate_hash() + hashed_key = sha256_hash(key) + self.db_set("reset_password_key", hashed_key) self.db_set("last_reset_password_key_generated_on", now_datetime()) url = "/update-password?key=" + key @@ -817,8 +819,9 @@ def _get_user_for_update_password(key, old_password): # verify old password result = frappe._dict() if key: + hashed_key = sha256_hash(key) user = frappe.db.get_value( - "User", {"reset_password_key": key}, ["name", "last_reset_password_key_generated_on"] + "User", {"reset_password_key": hashed_key}, ["name", "last_reset_password_key_generated_on"] ) result.user, last_reset_password_key_generated_on = user or (None, None) if result.user: @@ -909,12 +912,11 @@ def sign_up(email, full_name, redirect_to): @frappe.whitelist(allow_guest=True) @rate_limit(limit=get_password_reset_limit, seconds=60 * 60) -def reset_password(user): - if user == "Administrator": - return "not allowed" - +def reset_password(user: str) -> str: try: - user = frappe.get_doc("User", user) + user: User = frappe.get_doc("User", user) + if user.name == "Administrator": + return "not allowed" if not user.enabled: return "disabled" diff --git a/frappe/core/form_tour/user_list_tour/user_list_tour.json b/frappe/core/form_tour/user_list_tour/user_list_tour.json deleted file mode 100644 index 83ae481d259..00000000000 --- a/frappe/core/form_tour/user_list_tour/user_list_tour.json +++ /dev/null @@ -1,95 +0,0 @@ -{ - "creation": "2023-05-24 12:53:02.844582", - "dashboard_name": "", - "docstatus": 0, - "doctype": "Form Tour", - "first_document": 0, - "idx": 0, - "include_name_field": 0, - "is_standard": 1, - "list_name": "List", - "modified": "2023-05-24 13:21:29.552864", - "modified_by": "Administrator", - "module": "Core", - "name": "User List Tour", - "new_document_form": 0, - "owner": "Administrator", - "page_name": "", - "page_route": "[\"List\",\"User\",\"List\"]", - "reference_doctype": "User", - "report_name": "", - "save_on_complete": 0, - "steps": [ - { - "description": "List view shows all the documents for a particular DocType. Here you can see all the current enabled users in the system. ", - "element_selector": ".frappe-list", - "fieldtype": "0", - "has_next_condition": 0, - "hide_buttons": 0, - "is_table_field": 0, - "modal_trigger": 0, - "next_on_click": 0, - "offset_x": 0, - "offset_y": 0, - "popover_element": 0, - "position": "Top Center", - "title": "Users List", - "ui_tour": 1 - }, - { - "description": "These are filters. You can use them to narrow down list of records.", - "element_selector": ".standard-filter-section.flex", - "fieldtype": "0", - "has_next_condition": 0, - "hide_buttons": 0, - "is_table_field": 0, - "modal_trigger": 0, - "next_on_click": 1, - "offset_x": 0, - "offset_y": 0, - "popover_element": 0, - "position": "Bottom", - "title": "Filters", - "ui_tour": 1 - }, - { - "description": "When standard filters are not enough you can use advance filters. ", - "element_selector": ".filter-selector > .btn-group", - "fieldtype": "0", - "has_next_condition": 0, - "hide_buttons": 0, - "is_table_field": 0, - "modal_trigger": 0, - "next_on_click": 0, - "offset_x": 0, - "offset_y": 0, - "ondemand_description": "Advance filters are applied on fields with different operators. \n
\nClick on \"Apply Filters\" to continue.", - "popover_element": 0, - "position": "Left", - "title": "Advanced Filters", - "ui_tour": 1 - }, - { - "description": "Let's create a new user.", - "element_selector": ".btn-primary.primary-action", - "fieldtype": "0", - "has_next_condition": 0, - "hide_buttons": 1, - "is_table_field": 0, - "modal_trigger": 0, - "next_on_click": 1, - "offset_x": 0, - "offset_y": 0, - "parent_element_selector": "", - "popover_element": 0, - "position": "Bottom", - "title": "New User", - "ui_tour": 1 - } - ], - "title": "User List Tour", - "track_steps": 1, - "ui_tour": 1, - "view_name": "List", - "workspace_name": "" -} \ No newline at end of file diff --git a/frappe/desk/form_tour/main_workspace_tour/main_workspace_tour.json b/frappe/desk/form_tour/main_workspace_tour/main_workspace_tour.json deleted file mode 100644 index afd0583cfb1..00000000000 --- a/frappe/desk/form_tour/main_workspace_tour/main_workspace_tour.json +++ /dev/null @@ -1,79 +0,0 @@ -{ - "creation": "2023-05-18 12:08:23.196462", - "dashboard_name": "", - "docstatus": 0, - "doctype": "Form Tour", - "first_document": 0, - "idx": 0, - "include_name_field": 0, - "is_standard": 1, - "list_name": "", - "modified": "2023-05-24 12:43:43.741781", - "modified_by": "Administrator", - "module": "Desk", - "name": "Main Workspace Tour", - "new_document_form": 0, - "owner": "Administrator", - "page_name": "", - "page_route": "[\"Workspaces\",\"*\"]", - "reference_doctype": "", - "report_name": "", - "save_on_complete": 0, - "steps": [ - { - "description": "This is Awesomebar, it helps you to navigate anywhere in the system, find documents, reports, settings, create new records and many more things.", - "element_selector": "#navbar-search", - "fieldtype": "0", - "has_next_condition": 0, - "hide_buttons": 0, - "is_table_field": 0, - "modal_trigger": 0, - "next_on_click": 0, - "offset_x": 0, - "offset_y": 0, - "parent_element_selector": ".input-group.search-bar", - "popover_element": 0, - "position": "Left", - "title": "Awesomebar", - "ui_tour": 1 - }, - { - "description": "These are workspaces. Each module workspace provides insightful information and shortcuts on one page. \n\n

\n\nTip: You can build custom workspaces for your needs.", - "element_selector": ".col-lg-2.layout-side-section", - "fieldtype": "0", - "has_next_condition": 0, - "hide_buttons": 0, - "is_table_field": 0, - "modal_trigger": 0, - "next_on_click": 0, - "offset_x": 0, - "offset_y": 0, - "popover_element": 0, - "position": "Right", - "title": "Workspace List", - "ui_tour": 1 - }, - { - "description": "
Click to visit the Workspace
", - "element_selector": ".desk-sidebar-item.standard-sidebar-item > [title=\"Users\"]", - "fieldtype": "0", - "has_next_condition": 0, - "hide_buttons": 1, - "is_table_field": 0, - "modal_trigger": 0, - "next_form_tour": "New Tools Tour", - "next_on_click": 1, - "offset_x": 0, - "offset_y": 0, - "popover_element": 0, - "position": "Right", - "title": "Users Workspace", - "ui_tour": 1 - } - ], - "title": "Main Workspace Tour", - "track_steps": 1, - "ui_tour": 1, - "view_name": "Workspaces", - "workspace_name": "" -} \ No newline at end of file diff --git a/frappe/desk/form_tour/users_workspace_tour/users_workspace_tour.json b/frappe/desk/form_tour/users_workspace_tour/users_workspace_tour.json deleted file mode 100644 index 97159ba6e3d..00000000000 --- a/frappe/desk/form_tour/users_workspace_tour/users_workspace_tour.json +++ /dev/null @@ -1,62 +0,0 @@ -{ - "creation": "2023-05-24 12:50:23.740052", - "dashboard_name": "", - "docstatus": 0, - "doctype": "Form Tour", - "first_document": 0, - "idx": 0, - "include_name_field": 0, - "is_standard": 1, - "list_name": "", - "modified": "2023-05-24 13:01:56.539128", - "modified_by": "Administrator", - "module": "Desk", - "name": "Users Workspace Tour", - "new_document_form": 0, - "owner": "Administrator", - "page_name": "", - "page_route": "[\"Workspaces\",\"Users\"]", - "reference_doctype": "", - "report_name": "", - "save_on_complete": 0, - "steps": [ - { - "description": "This is Users Workspace. You'll find all shortcuts for user, roles and permission management here.", - "element_selector": ".codex-editor", - "fieldtype": "0", - "has_next_condition": 0, - "hide_buttons": 0, - "is_table_field": 0, - "modal_trigger": 0, - "next_on_click": 0, - "offset_x": 0, - "offset_y": 0, - "popover_element": 0, - "position": "Left", - "title": "Workspace", - "ui_tour": 1 - }, - { - "description": "This is a shortcut to User DocType. \n
\n\nLet's Click on the User shortcut to explore all users in System.", - "element_selector": "[shortcut_name=\"User\"]", - "fieldtype": "0", - "has_next_condition": 0, - "hide_buttons": 1, - "is_table_field": 0, - "modal_trigger": 0, - "next_form_tour": "User List Tour", - "next_on_click": 0, - "offset_x": 0, - "offset_y": 0, - "popover_element": 0, - "position": "Right", - "title": "Users Shortcut", - "ui_tour": 1 - } - ], - "title": "Users Workspace Tour", - "track_steps": 1, - "ui_tour": 1, - "view_name": "Workspaces", - "workspace_name": "Users" -} \ No newline at end of file diff --git a/frappe/tests/test_utils.py b/frappe/tests/test_utils.py index 0c852299468..247bd8c70f8 100644 --- a/frappe/tests/test_utils.py +++ b/frappe/tests/test_utils.py @@ -49,6 +49,7 @@ now_datetime, nowtime, rounded, + sha256_hash, validate_python_code, ) from frappe.utils.dateutils import get_dates_from_timegrain @@ -915,3 +916,12 @@ def test_bankers_rounding(self): @given(st.decimals(min_value=-1e8, max_value=1e8), st.integers(min_value=-2, max_value=4)) def test_bankers_rounding_property(self, number, precision): self.assertEqual(Decimal(str(flt(float(number), precision))), round(number, precision)) + + +class TestCrypto(FrappeTestCase): + def test_hashing(self): + self.assertEqual(sha256_hash(""), "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855") + self.assertEqual( + sha256_hash(b"The quick brown fox jumps over the lazy dog"), + "d7a8fbb307d7809469ca9abcb0082e4f8d5651e46d3cdb762d02d0bf37c9e592", + ) diff --git a/frappe/tests/utils.py b/frappe/tests/utils.py index 968d1a54fa7..34a45b87439 100644 --- a/frappe/tests/utils.py +++ b/frappe/tests/utils.py @@ -192,7 +192,7 @@ def _restore_thread_locals(flags): @contextmanager -def change_settings(doctype, settings_dict): +def change_settings(doctype, settings_dict=None, /, commit=False, **settings): """A context manager to ensure that settings are changed before running function and restored after running it regardless of exceptions occured. This is useful in tests where you want to make changes in a function but @@ -206,6 +206,8 @@ def test_case(self): """ try: + if settings_dict is None: + settings_dict = settings settings = frappe.get_doc(doctype) # remember setting previous_settings = copy.deepcopy(settings_dict) @@ -218,6 +220,8 @@ def test_case(self): settings.save(ignore_permissions=True) # singles are cached by default, clear to avoid flake frappe.db.value_cache[settings] = {} + if commit: + frappe.db.commit() yield # yield control to calling function finally: @@ -226,6 +230,8 @@ def test_case(self): for key, value in previous_settings.items(): setattr(settings, key, value) settings.save(ignore_permissions=True) + if commit: + frappe.db.commit() def timeout(seconds=30, error_message="Test timed out."): diff --git a/frappe/utils/data.py b/frappe/utils/data.py index e5e19ca7c22..2447c0d2b00 100644 --- a/frappe/utils/data.py +++ b/frappe/utils/data.py @@ -3,6 +3,7 @@ import base64 import datetime +import hashlib import json import math import operator @@ -2017,6 +2018,13 @@ def generate_hash(*args, **kwargs) -> str: return frappe.generate_hash(*args, **kwargs) +def sha256_hash(input: str | bytes) -> str: + """Return hash of the string using sha256 algorithm.""" + if isinstance(input, str): + input = input.encode() + return hashlib.sha256(input).hexdigest() + + def dict_with_keys(dict, keys): """Returns a new dict with a subset of keys""" out = {} diff --git a/frappe/www/update-password.html b/frappe/www/update-password.html index 2e6c3cf089e..29c5a1b7b34 100644 --- a/frappe/www/update-password.html +++ b/frappe/www/update-password.html @@ -167,9 +167,8 @@

{{ _("Reset Password") if frappe.db.get_defau window.timout_password_strength = setTimeout(window.test_password_strength, 200); }); - $("#old_password, #new_password, #confirm_password").on("keyup", frappe.utils.debounce(function () { - let common_conditions = new_password.val() && confirm_password.val() && new_password.val() === confirm_password.val() && - password_strength_message.text() === password_strength_message_success + $("#old_password, #new_password, #confirm_password").on("keyup paste", frappe.utils.debounce(function () { + let common_conditions = new_password.val() && confirm_password.val() && new_password.val() === confirm_password.val() if (new_password.val() && old_password.val() === new_password.val()) { password_mismatch_message.text(password_not_same_as_old_password) @@ -192,7 +191,7 @@

{{ _("Reset Password") if frappe.db.get_defau .removeClass("hidden text-muted").addClass("text-danger"); password_strength_message.addClass("hidden"); } - if ((key || (!key && old_password.val() && password_mismatch_message.text() !== password_not_same_as_old_password )) && common_conditions ) { + if ((key || (!key && old_password.val() )) && common_conditions ) { update_button.prop("disabled", false).css("cursor", "pointer"); } else { @@ -231,9 +230,13 @@

{{ _("Reset Password") if frappe.db.get_defau var score = r.message.score, feedback = r.message.feedback; + if (!feedback) { + return; + } + feedback.score = score; - if(feedback.password_policy_validation_passed){ + if (feedback.password_policy_validation_passed) { set_strength_indicator('green', feedback); }else{ set_strength_indicator('red', feedback); From 744b363fdeb0d23e486410dbe490541b3d1b21d4 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Mon, 19 Feb 2024 13:44:50 +0000 Subject: [PATCH 15/16] fix: set same cookie expiry on client side (backport #24560) (#24567) * fix: set same cookie expiry as client side (#24560) (cherry picked from commit 70a6a8334fb15af30afcd35fa387ec9b78ef1a8e) # Conflicts: # frappe/auth.py # frappe/sessions.py * chore: conflicts --------- Co-authored-by: Ankush Menat --- frappe/auth.py | 24 +++++++++++++++++------- frappe/tests/test_auth.py | 11 +++++++++-- 2 files changed, 26 insertions(+), 9 deletions(-) diff --git a/frappe/auth.py b/frappe/auth.py index 75e6f94b9bc..032e83883a7 100644 --- a/frappe/auth.py +++ b/frappe/auth.py @@ -2,14 +2,15 @@ # MIT License. See LICENSE from urllib.parse import quote +from werkzeug.wrappers import Response + import frappe import frappe.database import frappe.utils import frappe.utils.user from frappe import _ from frappe.core.doctype.activity_log.activity_log import add_authentication_log -from frappe.modules.patch_handler import check_session_stopped -from frappe.sessions import Session, clear_sessions, delete_session +from frappe.sessions import Session, clear_sessions, delete_session, get_expiry_in_seconds from frappe.translate import get_language from frappe.twofactor import ( authenticate_for_2factor, @@ -351,14 +352,21 @@ def init_cookies(self): if not frappe.local.session.get("sid"): return - # sid expires in 3 days - expires = datetime.datetime.now() + datetime.timedelta(days=3) if frappe.session.sid: - self.set_cookie("sid", frappe.session.sid, expires=expires, httponly=True) + self.set_cookie("sid", frappe.session.sid, max_age=get_expiry_in_seconds(), httponly=True) if frappe.session.session_country: self.set_cookie("country", frappe.session.session_country) - def set_cookie(self, key, value, expires=None, secure=False, httponly=False, samesite="Lax"): + def set_cookie( + self, + key, + value, + expires=None, + secure=False, + httponly=False, + samesite="Lax", + max_age=None, + ): if not secure and hasattr(frappe.local, "request"): secure = frappe.local.request.scheme == "https" @@ -372,6 +380,7 @@ def set_cookie(self, key, value, expires=None, secure=False, httponly=False, sam "secure": secure, "httponly": httponly, "samesite": samesite, + "max_age": max_age, } def delete_cookie(self, to_delete): @@ -380,7 +389,7 @@ def delete_cookie(self, to_delete): self.to_delete.extend(to_delete) - def flush_cookies(self, response): + def flush_cookies(self, response: Response): for key, opts in self.cookies.items(): response.set_cookie( key, @@ -389,6 +398,7 @@ def flush_cookies(self, response): secure=opts.get("secure"), httponly=opts.get("httponly"), samesite=opts.get("samesite"), + max_age=opts.get("max_age"), ) # expires yesterday! diff --git a/frappe/tests/test_auth.py b/frappe/tests/test_auth.py index 1736305da45..f7572509afb 100644 --- a/frappe/tests/test_auth.py +++ b/frappe/tests/test_auth.py @@ -1,7 +1,7 @@ # Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and Contributors # License: MIT. See LICENSE +import datetime import time -from unittest.mock import patch import requests @@ -11,7 +11,7 @@ from frappe.sessions import Session, get_expired_sessions, get_expiry_in_seconds from frappe.tests.test_api import FrappeAPITestCase from frappe.tests.utils import FrappeTestCase -from frappe.utils import get_site_url, now +from frappe.utils import get_datetime, get_site_url, now from frappe.utils.data import add_to_date from frappe.www.login import _generate_temporary_login_link @@ -157,6 +157,13 @@ def test_login_with_email_link(self): else: self.fail("Rate limting not working") + def test_correct_cookie_expiry_set(self): + client = FrappeClient(self.HOST_NAME, self.test_user_email, self.test_user_password) + + expiry_time = next(x for x in client.session.cookies if x.name == "sid").expires + current_time = datetime.datetime.utcnow().timestamp() + self.assertAlmostEqual(get_expiry_in_seconds(), expiry_time - current_time, delta=60 * 60) + class TestLoginAttemptTracker(FrappeTestCase): def test_account_lock(self): From b26f55cdecee03dd757d47478b16f02a4214ac4a Mon Sep 17 00:00:00 2001 From: Frappe PR Bot Date: Tue, 20 Feb 2024 10:15:46 +0000 Subject: [PATCH 16/16] chore(release): Bumped to Version 14.66.2 ## [14.66.2](https://github.com/frappe/frappe/compare/v14.66.1...v14.66.2) (2024-02-20) ### Bug Fixes * **calendar:** Replace route in load_last_view ([#24894](https://github.com/frappe/frappe/issues/24894)) ([#24927](https://github.com/frappe/frappe/issues/24927)) ([fa184d6](https://github.com/frappe/frappe/commit/fa184d65b4c9cb5c7c2e4c7a38b5c44ec34cc1de)) * ensure has_value_changed works for Datetime, Date and Time fields (backport [#24919](https://github.com/frappe/frappe/issues/24919)) ([#24921](https://github.com/frappe/frappe/issues/24921)) ([8087a7a](https://github.com/frappe/frappe/commit/8087a7a58a011425029cfb4ca32c47a71d32d1a3)) * handle bad cron expressions (backport [#24938](https://github.com/frappe/frappe/issues/24938)) ([#24941](https://github.com/frappe/frappe/issues/24941)) ([4efcdb5](https://github.com/frappe/frappe/commit/4efcdb506a7f9243c702ec1aedf3ea0dd83f3421)) * ignore and gracefully handle img optimization failure ([b112333](https://github.com/frappe/frappe/commit/b112333f32eeb1f5652b5f7f7c1772a3b7033847)) * invite contact as user ([136ac70](https://github.com/frappe/frappe/commit/136ac70d19b7187fed88eaac04665753ba4f010e)) * keep order in `get_values_from_single` ([#24907](https://github.com/frappe/frappe/issues/24907)) ([#24923](https://github.com/frappe/frappe/issues/24923)) ([3347144](https://github.com/frappe/frappe/commit/3347144f040171fd1bf4adddd641635d4f704edf)) * lower socket timeout for validating email domain ([#24915](https://github.com/frappe/frappe/issues/24915)) ([#24917](https://github.com/frappe/frappe/issues/24917)) ([427e681](https://github.com/frappe/frappe/commit/427e68190dd1c0be8da029aacbd535e4de42d94b)) * **page:** Catch LocalStorage quota exception ([#24885](https://github.com/frappe/frappe/issues/24885)) ([#24930](https://github.com/frappe/frappe/issues/24930)) ([3a36f7d](https://github.com/frappe/frappe/commit/3a36f7d743345fd05c2820d021855e5d4d694afc)) * send_workflow_action_email ([#24929](https://github.com/frappe/frappe/issues/24929)) ([#24933](https://github.com/frappe/frappe/issues/24933)) ([5f29c83](https://github.com/frappe/frappe/commit/5f29c836c6463805a4b0458772286a642cd68013)) * set same cookie expiry on client side (backport [#24560](https://github.com/frappe/frappe/issues/24560)) ([#24567](https://github.com/frappe/frappe/issues/24567)) ([744b363](https://github.com/frappe/frappe/commit/744b363fdeb0d23e486410dbe490541b3d1b21d4)) * use communication date in timeline ([0632cb3](https://github.com/frappe/frappe/commit/0632cb397252b37912ba71d37488e7e60367f281)) * **weblist:** Fix "More" button not working ([#24893](https://github.com/frappe/frappe/issues/24893)) ([#24925](https://github.com/frappe/frappe/issues/24925)) ([1715638](https://github.com/frappe/frappe/commit/17156384c44b6a9db61f08480e40f94f6ebb5a14)) --- frappe/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frappe/__init__.py b/frappe/__init__.py index d8f1154b924..83bf61671db 100644 --- a/frappe/__init__.py +++ b/frappe/__init__.py @@ -44,7 +44,7 @@ ) from .utils.lazy_loader import lazy_import -__version__ = "14.66.1" +__version__ = "14.66.2" __title__ = "Frappe Framework" controllers = {}