You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 

377 lines
10 KiB

  1. # Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
  2. # License: MIT. See LICENSE
  3. import functools
  4. import json
  5. import re
  6. import frappe
  7. from frappe import _, is_whitelisted
  8. from frappe.permissions import has_permission
  9. from frappe.utils import cint, cstr, unique
  10. def sanitize_searchfield(searchfield):
  11. blacklisted_keywords = ["select", "delete", "drop", "update", "case", "and", "or", "like"]
  12. def _raise_exception(searchfield):
  13. frappe.throw(_("Invalid Search Field {0}").format(searchfield), frappe.DataError)
  14. if len(searchfield) == 1:
  15. # do not allow special characters to pass as searchfields
  16. regex = re.compile(r'^.*[=;*,\'"$\-+%#@()_].*')
  17. if regex.match(searchfield):
  18. _raise_exception(searchfield)
  19. if len(searchfield) >= 3:
  20. # to avoid 1=1
  21. if "=" in searchfield:
  22. _raise_exception(searchfield)
  23. # in mysql -- is used for commenting the query
  24. elif " --" in searchfield:
  25. _raise_exception(searchfield)
  26. # to avoid and, or and like
  27. elif any(" {0} ".format(keyword) in searchfield.split() for keyword in blacklisted_keywords):
  28. _raise_exception(searchfield)
  29. # to avoid select, delete, drop, update and case
  30. elif any(keyword in searchfield.split() for keyword in blacklisted_keywords):
  31. _raise_exception(searchfield)
  32. else:
  33. regex = re.compile(r'^.*[=;*,\'"$\-+%#@()].*')
  34. if any(regex.match(f) for f in searchfield.split()):
  35. _raise_exception(searchfield)
  36. # this is called by the Link Field
  37. @frappe.whitelist()
  38. def search_link(
  39. doctype,
  40. txt,
  41. query=None,
  42. filters=None,
  43. page_length=20,
  44. searchfield=None,
  45. reference_doctype=None,
  46. ignore_user_permissions=False,
  47. ):
  48. search_widget(
  49. doctype,
  50. txt.strip(),
  51. query,
  52. searchfield=searchfield,
  53. page_length=page_length,
  54. filters=filters,
  55. reference_doctype=reference_doctype,
  56. ignore_user_permissions=ignore_user_permissions,
  57. )
  58. frappe.response["results"] = build_for_autosuggest(frappe.response["values"], doctype=doctype)
  59. del frappe.response["values"]
  60. # this is called by the search box
  61. @frappe.whitelist()
  62. def search_widget(
  63. doctype,
  64. txt,
  65. query=None,
  66. searchfield=None,
  67. start=0,
  68. page_length=20,
  69. filters=None,
  70. filter_fields=None,
  71. as_dict=False,
  72. reference_doctype=None,
  73. ignore_user_permissions=False,
  74. ):
  75. start = cint(start)
  76. if isinstance(filters, str):
  77. filters = json.loads(filters)
  78. if searchfield:
  79. sanitize_searchfield(searchfield)
  80. if not searchfield:
  81. searchfield = "name"
  82. standard_queries = frappe.get_hooks().standard_queries or {}
  83. if query and query.split()[0].lower() != "select":
  84. # by method
  85. try:
  86. is_whitelisted(frappe.get_attr(query))
  87. frappe.response["values"] = frappe.call(
  88. query, doctype, txt, searchfield, start, page_length, filters, as_dict=as_dict
  89. )
  90. except frappe.exceptions.PermissionError as e:
  91. if frappe.local.conf.developer_mode:
  92. raise e
  93. else:
  94. frappe.respond_as_web_page(
  95. title="Invalid Method", html="Method not found", indicator_color="red", http_status_code=404
  96. )
  97. return
  98. except Exception as e:
  99. raise e
  100. elif not query and doctype in standard_queries:
  101. # from standard queries
  102. search_widget(
  103. doctype, txt, standard_queries[doctype][0], searchfield, start, page_length, filters
  104. )
  105. else:
  106. meta = frappe.get_meta(doctype)
  107. if query:
  108. frappe.throw(_("This query style is discontinued"))
  109. # custom query
  110. # frappe.response["values"] = frappe.db.sql(scrub_custom_query(query, searchfield, txt))
  111. else:
  112. if isinstance(filters, dict):
  113. filters_items = filters.items()
  114. filters = []
  115. for f in filters_items:
  116. if isinstance(f[1], (list, tuple)):
  117. filters.append([doctype, f[0], f[1][0], f[1][1]])
  118. else:
  119. filters.append([doctype, f[0], "=", f[1]])
  120. if filters is None:
  121. filters = []
  122. or_filters = []
  123. translated_search_doctypes = frappe.get_hooks("translated_search_doctypes")
  124. # build from doctype
  125. if txt:
  126. search_fields = ["name"]
  127. if meta.title_field:
  128. search_fields.append(meta.title_field)
  129. if meta.search_fields:
  130. search_fields.extend(meta.get_search_fields())
  131. for f in search_fields:
  132. fmeta = meta.get_field(f.strip())
  133. if (doctype not in translated_search_doctypes) and (
  134. f == "name"
  135. or (
  136. fmeta
  137. and fmeta.fieldtype
  138. in ["Data", "Text", "Small Text", "Long Text", "Link", "Select", "Read Only", "Text Editor"]
  139. )
  140. ):
  141. or_filters.append([doctype, f.strip(), "like", "%{0}%".format(txt)])
  142. if meta.get("fields", {"fieldname": "enabled", "fieldtype": "Check"}):
  143. filters.append([doctype, "enabled", "=", 1])
  144. if meta.get("fields", {"fieldname": "disabled", "fieldtype": "Check"}):
  145. filters.append([doctype, "disabled", "!=", 1])
  146. # format a list of fields combining search fields and filter fields
  147. fields = get_std_fields_list(meta, searchfield or "name")
  148. if filter_fields:
  149. fields = list(set(fields + json.loads(filter_fields)))
  150. formatted_fields = ["`tab%s`.`%s`" % (meta.name, f.strip()) for f in fields]
  151. title_field_query = get_title_field_query(meta)
  152. # Insert title field query after name
  153. if title_field_query:
  154. formatted_fields.insert(1, title_field_query)
  155. # find relevance as location of search term from the beginning of string `name`. used for sorting results.
  156. formatted_fields.append(
  157. """locate({_txt}, `tab{doctype}`.`name`) as `_relevance`""".format(
  158. _txt=frappe.db.escape((txt or "").replace("%", "").replace("@", "")), doctype=doctype
  159. )
  160. )
  161. # In order_by, `idx` gets second priority, because it stores link count
  162. from frappe.model.db_query import get_order_by
  163. order_by_based_on_meta = get_order_by(doctype, meta)
  164. # 2 is the index of _relevance column
  165. order_by = "_relevance, {0}, `tab{1}`.idx desc".format(order_by_based_on_meta, doctype)
  166. ptype = "select" if frappe.only_has_select_perm(doctype) else "read"
  167. ignore_permissions = (
  168. True
  169. if doctype == "DocType"
  170. else (cint(ignore_user_permissions) and has_permission(doctype, ptype=ptype))
  171. )
  172. if doctype in translated_search_doctypes:
  173. page_length = None
  174. values = frappe.get_list(
  175. doctype,
  176. filters=filters,
  177. fields=formatted_fields,
  178. or_filters=or_filters,
  179. limit_start=start,
  180. limit_page_length=page_length,
  181. order_by=order_by,
  182. ignore_permissions=ignore_permissions,
  183. reference_doctype=reference_doctype,
  184. as_list=not as_dict,
  185. strict=False,
  186. )
  187. if doctype in translated_search_doctypes:
  188. # Filtering the values array so that query is included in very element
  189. values = (
  190. v
  191. for v in values
  192. if re.search(f"{re.escape(txt)}.*", _(v.name if as_dict else v[0]), re.IGNORECASE)
  193. )
  194. # Sorting the values array so that relevant results always come first
  195. # This will first bring elements on top in which query is a prefix of element
  196. # Then it will bring the rest of the elements and sort them in lexicographical order
  197. values = sorted(values, key=lambda x: relevance_sorter(x, txt, as_dict))
  198. # remove _relevance from results
  199. if as_dict:
  200. for r in values:
  201. r.pop("_relevance")
  202. frappe.response["values"] = values
  203. else:
  204. frappe.response["values"] = [r[:-1] for r in values]
  205. def get_std_fields_list(meta, key):
  206. # get additional search fields
  207. sflist = ["name"]
  208. if meta.search_fields:
  209. for d in meta.search_fields.split(","):
  210. if d.strip() not in sflist:
  211. sflist.append(d.strip())
  212. if meta.title_field and meta.title_field not in sflist:
  213. sflist.append(meta.title_field)
  214. if key not in sflist:
  215. sflist.append(key)
  216. return sflist
  217. def get_title_field_query(meta):
  218. title_field = meta.title_field if meta.title_field else None
  219. show_title_field_in_link = (
  220. meta.show_title_field_in_link if meta.show_title_field_in_link else None
  221. )
  222. field = None
  223. if title_field and show_title_field_in_link:
  224. field = "`tab{0}`.{1} as `label`".format(meta.name, title_field)
  225. return field
  226. def build_for_autosuggest(res, doctype):
  227. results = []
  228. meta = frappe.get_meta(doctype)
  229. if not (meta.title_field and meta.show_title_field_in_link):
  230. for r in res:
  231. r = list(r)
  232. results.append({"value": r[0], "description": ", ".join(unique(cstr(d) for d in r[1:] if d))})
  233. else:
  234. title_field_exists = meta.title_field and meta.show_title_field_in_link
  235. _from = 2 if title_field_exists else 1 # to exclude title from description if title_field_exists
  236. for r in res:
  237. r = list(r)
  238. results.append(
  239. {
  240. "value": r[0],
  241. "label": r[1] if title_field_exists else None,
  242. "description": ", ".join(unique(cstr(d) for d in r[_from:] if d)),
  243. }
  244. )
  245. return results
  246. def scrub_custom_query(query, key, txt):
  247. if "%(key)s" in query:
  248. query = query.replace("%(key)s", key)
  249. if "%s" in query:
  250. query = query.replace("%s", ((txt or "") + "%"))
  251. return query
  252. def relevance_sorter(key, query, as_dict):
  253. value = _(key.name if as_dict else key[0])
  254. return (cstr(value).lower().startswith(query.lower()) is not True, value)
  255. def validate_and_sanitize_search_inputs(fn):
  256. @functools.wraps(fn)
  257. def wrapper(*args, **kwargs):
  258. kwargs.update(dict(zip(fn.__code__.co_varnames, args)))
  259. sanitize_searchfield(kwargs["searchfield"])
  260. kwargs["start"] = cint(kwargs["start"])
  261. kwargs["page_len"] = cint(kwargs["page_len"])
  262. if kwargs["doctype"] and not frappe.db.exists("DocType", kwargs["doctype"]):
  263. return []
  264. return fn(**kwargs)
  265. return wrapper
  266. @frappe.whitelist()
  267. def get_names_for_mentions(search_term):
  268. users_for_mentions = frappe.cache().get_value("users_for_mentions", get_users_for_mentions)
  269. user_groups = frappe.cache().get_value("user_groups", get_user_groups)
  270. filtered_mentions = []
  271. for mention_data in users_for_mentions + user_groups:
  272. if search_term.lower() not in mention_data.value.lower():
  273. continue
  274. mention_data["link"] = frappe.utils.get_url_to_form(
  275. "User Group" if mention_data.get("is_group") else "User Profile", mention_data["id"]
  276. )
  277. filtered_mentions.append(mention_data)
  278. return sorted(filtered_mentions, key=lambda d: d["value"])
  279. def get_users_for_mentions():
  280. return frappe.get_all(
  281. "User",
  282. fields=["name as id", "full_name as value"],
  283. filters={
  284. "name": ["not in", ("Administrator", "Guest")],
  285. "allowed_in_mentions": True,
  286. "user_type": "System User",
  287. "enabled": True,
  288. },
  289. )
  290. def get_user_groups():
  291. return frappe.get_all(
  292. "User Group", fields=["name as id", "name as value"], update={"is_group": True}
  293. )
  294. @frappe.whitelist()
  295. def get_link_title(doctype, docname):
  296. meta = frappe.get_meta(doctype)
  297. if meta.title_field and meta.show_title_field_in_link:
  298. return frappe.db.get_value(doctype, docname, meta.title_field)
  299. return docname