diff --git a/frappe/auth.py b/frappe/auth.py index 5a447a99af..1658930317 100644 --- a/frappe/auth.py +++ b/frappe/auth.py @@ -253,7 +253,11 @@ class LoginManager: ): return - clear_sessions(frappe.session.user, keep_current=True) + clear_sessions( + frappe.session.user, + keep_current=True, + force=frappe.session.user != "Administrator", + ) def authenticate(self, user: str | None = None, pwd: str | None = None): from frappe.core.doctype.user.user import User diff --git a/frappe/core/doctype/system_settings/system_settings.json b/frappe/core/doctype/system_settings/system_settings.json index c1aa3fb0d7..75d1bd978a 100644 --- a/frappe/core/doctype/system_settings/system_settings.json +++ b/frappe/core/doctype/system_settings/system_settings.json @@ -248,7 +248,6 @@ }, { "default": "0", - "description": "Note: Multiple sessions will be allowed in case of mobile device", "fieldname": "deny_multiple_sessions", "fieldtype": "Check", "label": "Allow only one session per user" @@ -790,7 +789,7 @@ "icon": "fa fa-cog", "issingle": 1, "links": [], - "modified": "2026-01-02 18:13:45.430712", + "modified": "2026-02-24 14:27:04.763075", "modified_by": "Administrator", "module": "Core", "name": "System Settings", diff --git a/frappe/desk/doctype/workspace/workspace.py b/frappe/desk/doctype/workspace/workspace.py index 29d9cb6b0f..1424e2a3d8 100644 --- a/frappe/desk/doctype/workspace/workspace.py +++ b/frappe/desk/doctype/workspace/workspace.py @@ -76,6 +76,18 @@ class Workspace(Document): if self.public and not is_workspace_manager() and not disable_saving_as_public(): frappe.throw(_("You need to be Workspace Manager to edit this document")) + + if ( + not self.public + and self.for_user + and self.for_user != frappe.session.user + and not is_workspace_manager() + ): + frappe.throw( + _("You are not allowed to edit this workspace"), + frappe.PermissionError, + ) + if self.has_value_changed("title"): validate_route_conflict(self.doctype, self.title) else: diff --git a/frappe/email/doctype/email_queue/email_queue.py b/frappe/email/doctype/email_queue/email_queue.py index b0576dfe38..ca05ec6278 100644 --- a/frappe/email/doctype/email_queue/email_queue.py +++ b/frappe/email/doctype/email_queue/email_queue.py @@ -514,7 +514,18 @@ def send_now(name: str | int, force_send: bool = False): @frappe.whitelist() def toggle_sending(enable: bool | int | str): frappe.only_for("System Manager") - frappe.db.set_default("suspend_email_queue", 0 if sbool(enable) else 1) + suspend_value = 0 if sbool(enable) else 1 + frappe.db.set_default("suspend_email_queue", suspend_value) + + action = "Resumed" if suspend_value == 0 else "Suspended" + frappe.get_doc( + { + "doctype": "Activity Log", + "user": frappe.session.user, + "status": "Success", + "subject": f"Email Queue sending {action.lower()}", + } + ).insert(ignore_permissions=True, ignore_links=True) def on_doctype_update(): diff --git a/frappe/oauth.py b/frappe/oauth.py index 5e170db5d2..67595555f9 100644 --- a/frappe/oauth.py +++ b/frappe/oauth.py @@ -2,13 +2,14 @@ import base64 import datetime import hashlib import re -from http import cookies -from urllib.parse import unquote, urljoin, urlparse +from urllib.parse import urljoin, urlparse +from oauthlib.common import Request from oauthlib.openid import RequestValidator import frappe from frappe.auth import LoginManager +from frappe.integrations.doctype.oauth_client.oauth_client import OAuthClient from frappe.utils.data import cstr, get_system_timezone, now_datetime @@ -73,13 +74,11 @@ class OAuthWebRequestValidator(RequestValidator): # Post-authorization def save_authorization_code(self, client_id, code, request, *args, **kwargs): - cookie_dict = get_cookie_dict_from_headers(request) - oac = frappe.new_doc("OAuth Authorization Code") oac.scopes = get_url_delimiter().join(request.scopes) oac.redirect_uri_bound_to_authorization_code = request.redirect_uri oac.client = client_id - oac.user = unquote(cookie_dict["user_id"].value) + oac.user = frappe.session.user oac.authorization_code = code["code"] if request.nonce: @@ -92,43 +91,32 @@ class OAuthWebRequestValidator(RequestValidator): oac.save(ignore_permissions=True) frappe.db.commit() - def authenticate_client(self, request, *args, **kwargs): + def authenticate_client(self, request: Request, *args, **kwargs) -> bool | None: + """ + Loads the client based on request parameters and sets in oauth request. + Returns True on success, None on error. + """ # Get ClientID in URL if request.client_id: - oc = frappe.get_doc("OAuth Client", request.client_id) + client_name = request.client_id else: # Extract token, instantiate OAuth Bearer Token and use clientid from there. if "refresh_token" in frappe.form_dict: - oc = frappe.get_doc( - "OAuth Client", - frappe.db.get_value( - "OAuth Bearer Token", - {"refresh_token": frappe.form_dict["refresh_token"]}, - "client", - ), - ) + token_filters = {"refresh_token": frappe.form_dict["refresh_token"]} elif "token" in frappe.form_dict: - oc = frappe.get_doc( - "OAuth Client", - frappe.db.get_value("OAuth Bearer Token", frappe.form_dict["token"], "client"), - ) + token_filters = {"name": frappe.form_dict["token"]} else: - oc = frappe.get_doc( - "OAuth Client", - frappe.db.get_value( - "OAuth Bearer Token", - frappe.get_request_header("Authorization").split(" ")[1], - "client", - ), - ) + token_filters = {"name": frappe.get_request_header("Authorization").split(" ")[1]} + + client_name = frappe.db.get_value("OAuth Bearer Token", filters=token_filters, fieldname="client") + + oc: OAuthClient = frappe.get_doc("OAuth Client", client_name) try: request.client = request.client or oc.as_dict() except Exception as e: return generate_json_error_response(e) - cookie_dict = get_cookie_dict_from_headers(request) - user_id = unquote(cookie_dict.get("user_id").value) if "user_id" in cookie_dict else "Guest" - return frappe.session.user == user_id + return True def authenticate_client_id(self, client_id, request, *args, **kwargs): cli_id = frappe.db.get_value("OAuth Client", client_id, "name") @@ -506,13 +494,6 @@ class OAuthWebRequestValidator(RequestValidator): return True -def get_cookie_dict_from_headers(r): - cookie = cookies.BaseCookie() - if r.headers.get("Cookie"): - cookie.load(r.headers.get("Cookie")) - return cookie - - def calculate_at_hash(access_token, hash_alg): """Helper method for calculating an access token hash, as described in http://openid.net/specs/openid-connect-core-1_0.html#CodeIDToken diff --git a/frappe/printing/doctype/letter_head/letter_head.json b/frappe/printing/doctype/letter_head/letter_head.json index 9855c0ff32..4ddafc47db 100644 --- a/frappe/printing/doctype/letter_head/letter_head.json +++ b/frappe/printing/doctype/letter_head/letter_head.json @@ -98,6 +98,7 @@ "description": "Letter Head in HTML", "fieldname": "content", "fieldtype": "HTML Editor", + "ignore_xss_filter": 1, "label": "Header HTML", "oldfieldname": "content", "oldfieldtype": "Text Editor" @@ -113,6 +114,7 @@ "description": "Footer will display correctly only in PDF", "fieldname": "footer", "fieldtype": "HTML Editor", + "ignore_xss_filter": 1, "label": "Footer HTML" }, { @@ -184,6 +186,7 @@ { "collapsible": 1, "collapsible_depends_on": "eval: doc.header_script || doc.footer_script", + "depends_on": "eval: !doc.__islocal", "fieldname": "scripts_section", "fieldtype": "Section Break", "label": "Scripts" @@ -200,7 +203,7 @@ "links": [], "make_attachments_public": 1, "max_attachments": 3, - "modified": "2024-04-12 10:30:25.793932", + "modified": "2026-02-24 20:53:14.297567", "modified_by": "Administrator", "module": "Printing", "name": "Letter Head", @@ -223,8 +226,9 @@ "role": "Desk User" } ], + "row_format": "Dynamic", "sort_field": "creation", "sort_order": "DESC", "states": [], "track_changes": 1 -} \ No newline at end of file +} diff --git a/frappe/printing/page/print/print.js b/frappe/printing/page/print/print.js index f2f69e3aa3..0e5d68c165 100644 --- a/frappe/printing/page/print/print.js +++ b/frappe/printing/page/print/print.js @@ -706,7 +706,10 @@ frappe.ui.form.PrintView = class { return; } } else { - this.is_wkhtmltopdf_valid(); + let pdf_generator = this.get_pdf_generator(print_format?.pdf_generator); + if (pdf_generator === "wkhtmltopdf") { + this.is_wkhtmltopdf_valid(); + } this.render_page( "/api/method/frappe.utils.print_format.download_pdf?", false, @@ -738,9 +741,7 @@ frappe.ui.form.PrintView = class { encodeURIComponent(this.get_letterhead()) + "&settings=" + encodeURIComponent(JSON.stringify(this.additional_settings)) + - (this.lang_code ? "&_lang=" + this.lang_code : "") + - "&pdf_generator=" + - encodeURIComponent(pdf_generator) + (this.lang_code ? "&_lang=" + this.lang_code : "") ) ); if (!w) { diff --git a/frappe/public/js/frappe/list/list_view.js b/frappe/public/js/frappe/list/list_view.js index d9c6514556..2c28a24063 100644 --- a/frappe/public/js/frappe/list/list_view.js +++ b/frappe/public/js/frappe/list/list_view.js @@ -291,8 +291,9 @@ frappe.views.ListView = class ListView extends frappe.views.BaseList { set_primary_action() { if (this.can_create && !frappe.boot.read_only) { const doctype_name = __(frappe.router.doctype_layout) || __(this.doctype); + const add_button_label = __("Add {0}", [doctype_name], "Primary action in list view"); const create_button = this.page.set_primary_action( - __("Add {0}", [doctype_name], "Primary action in list view"), + add_button_label, () => { if (this.settings.primary_action) { this.settings.primary_action(); @@ -304,12 +305,26 @@ frappe.views.ListView = class ListView extends frappe.views.BaseList { ); if (frappe.is_mobile()) { create_button.append(__("Add")); + } else { + this._trim_primary_action_if_overflow(create_button, add_button_label); } } else { this.page.clear_primary_action(); } } + _trim_primary_action_if_overflow(btn, add_button_label) { + const container = this.page.wrapper.find(".page-head-content")[0]; + if (!container || !btn[0]) return; + const containerRect = container.getBoundingClientRect(); + const btnRect = btn[0].getBoundingClientRect(); + if (btnRect.right > containerRect.right) { + const short_label = __("Add"); + btn.attr("title", add_button_label).tooltip(); + btn.find("span").text(short_label); + } + } + make_new_doc() { const doctype = this.doctype; const options = {}; diff --git a/frappe/public/js/frappe/ui/sidebar/sidebar.html b/frappe/public/js/frappe/ui/sidebar/sidebar.html index ef5e6d630a..72498d5327 100644 --- a/frappe/public/js/frappe/ui/sidebar/sidebar.html +++ b/frappe/public/js/frappe/ui/sidebar/sidebar.html @@ -43,7 +43,7 @@
diff --git a/frappe/public/js/frappe/ui/user_onboarding/OnboardingPanel.vue b/frappe/public/js/frappe/ui/user_onboarding/OnboardingPanel.vue index 68150e9744..17dde86cc6 100644 --- a/frappe/public/js/frappe/ui/user_onboarding/OnboardingPanel.vue +++ b/frappe/public/js/frappe/ui/user_onboarding/OnboardingPanel.vue @@ -170,6 +170,7 @@ function updateSettings(step) { }; frappe.set_route("Form", step.reference_document); + markComplete(step); } async function createEntry(step) { @@ -263,10 +264,10 @@ function markReset(step) {