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.
 
 
 
 
 
 

318 line
10 KiB

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