From e0a3ed5eff15662b3c13ec2843363c305933f715 Mon Sep 17 00:00:00 2001 From: Sagar Vora <16315650+sagarvora@users.noreply.github.com> Date: Sat, 10 Jan 2026 13:56:07 +0530 Subject: [PATCH] fix: validate for virtual fields, assume valid in some cases --- frappe/client.py | 39 ++++++++++++++++++++++++--------------- frappe/desk/search.py | 4 +++- 2 files changed, 27 insertions(+), 16 deletions(-) diff --git a/frappe/client.py b/frappe/client.py index 84e0ce878e..a1ee54fa46 100644 --- a/frappe/client.py +++ b/frappe/client.py @@ -9,7 +9,7 @@ import frappe.model import frappe.utils from frappe import _ from frappe.desk.reportview import validate_args -from frappe.desk.search import make_filter_tuple, search_widget +from frappe.desk.search import PAGE_LENGTH_FOR_LINK_VALIDATION, search_widget from frappe.model.utils import is_virtual_doctype from frappe.utils import attach_expanded_links, get_safe_filters from frappe.utils.caching import http_cache @@ -420,16 +420,6 @@ def validate_link_and_fetch( if not docname: frappe.throw(_("Document Name must not be empty")) - if is_virtual_doctype(doctype): - try: - doc = frappe.get_doc(doctype, docname) - doc.check_permission("select" if frappe.only_has_select_perm(doctype) else "read") - return {"name": doc.name} - - except frappe.DoesNotExistError: - frappe.clear_last_message() - return {} - fields_to_fetch = frappe.parse_json(fields_to_fetch) # only cache is no fields to fetch and request is GET @@ -456,18 +446,37 @@ def validate_link_and_fetch( if frappe.is_table(doctype): columns_to_fetch.append("parenttype") # for child table permission check - values = frappe.db.get_value(doctype, docname, columns_to_fetch, as_dict=True) - name_to_compare = values.name + is_virtual_dt = is_virtual_doctype(doctype) + if is_virtual_dt: + try: + doc = frappe.get_doc(doctype, docname) + doc.check_permission("select" if frappe.only_has_select_perm(doctype) else "read") + values = {"name": doc.name} + + except frappe.DoesNotExistError: + frappe.clear_last_message() + return {} + else: + values = frappe.db.get_value(doctype, docname, columns_to_fetch, as_dict=True) + + name_to_compare = values["name"] # this will be used to fetch fields later parent_doctype = values.pop("parenttype", None) if not name_to_compare: return {} # does not exist - # for custom queries that don't respect filters - if not any(item[0] == name_to_compare for item in search_result): + # try to match name in search result + # if search_result is large, assume valid link (result may not appear in some custom queries) + if len(search_result) < PAGE_LENGTH_FOR_LINK_VALIDATION and not any( + item[0] == name_to_compare for item in search_result + ): return {} # no permission or filtered out + # don't cache or fetch for virtual doctypes + if is_virtual_dt: + return values + if not fields_to_fetch: if can_cache: frappe.local.response_headers.set( diff --git a/frappe/desk/search.py b/frappe/desk/search.py index 9e1a8fa985..d18ce76748 100644 --- a/frappe/desk/search.py +++ b/frappe/desk/search.py @@ -16,6 +16,8 @@ from frappe.utils import cint, cstr, escape_html, unique from frappe.utils.caching import http_cache from frappe.utils.data import make_filter_tuple +PAGE_LENGTH_FOR_LINK_VALIDATION = 25_000 + def sanitize_searchfield(searchfield: str): if not searchfield: @@ -130,7 +132,7 @@ def search_widget( # for custom queries that don't respect filters but respect limit (rare) # or for when we have to rely on txt # we want to match "A" with "A" only and not "A1", "BA" etc. - page_length = 100_000 + page_length = PAGE_LENGTH_FOR_LINK_VALIDATION if query: # Query = custom search query i.e. python function try: