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.
 
 
 
 
 
 

258 line
8.4 KiB

  1. # Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
  2. # MIT License. See license.txt
  3. # Search
  4. from __future__ import unicode_literals
  5. import frappe, json
  6. from frappe.utils import cstr, unique, cint
  7. from frappe.permissions import has_permission
  8. from frappe import _, is_whitelisted
  9. from six import string_types
  10. import re
  11. import wrapt
  12. UNTRANSLATED_DOCTYPES = ["DocType", "Role"]
  13. def sanitize_searchfield(searchfield):
  14. blacklisted_keywords = ['select', 'delete', 'drop', 'update', 'case', 'and', 'or', 'like']
  15. def _raise_exception(searchfield):
  16. frappe.throw(_('Invalid Search Field {0}').format(searchfield), frappe.DataError)
  17. if len(searchfield) == 1:
  18. # do not allow special characters to pass as searchfields
  19. regex = re.compile(r'^.*[=;*,\'"$\-+%#@()_].*')
  20. if regex.match(searchfield):
  21. _raise_exception(searchfield)
  22. if len(searchfield) >= 3:
  23. # to avoid 1=1
  24. if '=' in searchfield:
  25. _raise_exception(searchfield)
  26. # in mysql -- is used for commenting the query
  27. elif ' --' in searchfield:
  28. _raise_exception(searchfield)
  29. # to avoid and, or and like
  30. elif any(' {0} '.format(keyword) in searchfield.split() for keyword in blacklisted_keywords):
  31. _raise_exception(searchfield)
  32. # to avoid select, delete, drop, update and case
  33. elif any(keyword in searchfield.split() for keyword in blacklisted_keywords):
  34. _raise_exception(searchfield)
  35. else:
  36. regex = re.compile(r'^.*[=;*,\'"$\-+%#@()].*')
  37. if any(regex.match(f) for f in searchfield.split()):
  38. _raise_exception(searchfield)
  39. # this is called by the Link Field
  40. @frappe.whitelist()
  41. def search_link(doctype, txt, query=None, filters=None, page_length=20, searchfield=None, reference_doctype=None, ignore_user_permissions=False):
  42. search_widget(doctype, txt.strip(), query, searchfield=searchfield, page_length=page_length, filters=filters, reference_doctype=reference_doctype, ignore_user_permissions=ignore_user_permissions)
  43. frappe.response['results'] = build_for_autosuggest(frappe.response["values"])
  44. del frappe.response["values"]
  45. # this is called by the search box
  46. @frappe.whitelist()
  47. def search_widget(doctype, txt, query=None, searchfield=None, start=0,
  48. page_length=20, filters=None, filter_fields=None, as_dict=False, reference_doctype=None, ignore_user_permissions=False):
  49. start = cint(start)
  50. if isinstance(filters, string_types):
  51. filters = json.loads(filters)
  52. if searchfield:
  53. sanitize_searchfield(searchfield)
  54. if not searchfield:
  55. searchfield = "name"
  56. standard_queries = frappe.get_hooks().standard_queries or {}
  57. if query and query.split()[0].lower()!="select":
  58. # by method
  59. try:
  60. is_whitelisted(frappe.get_attr(query))
  61. frappe.response["values"] = frappe.call(query, doctype, txt,
  62. searchfield, start, page_length, filters, as_dict=as_dict)
  63. except frappe.exceptions.PermissionError as e:
  64. if frappe.local.conf.developer_mode:
  65. raise e
  66. else:
  67. frappe.respond_as_web_page(title='Invalid Method', html='Method not found',
  68. indicator_color='red', http_status_code=404)
  69. return
  70. except Exception as e:
  71. raise e
  72. elif not query and doctype in standard_queries:
  73. # from standard queries
  74. search_widget(doctype, txt, standard_queries[doctype][0],
  75. searchfield, start, page_length, filters)
  76. else:
  77. meta = frappe.get_meta(doctype)
  78. if query:
  79. frappe.throw(_("This query style is discontinued"))
  80. # custom query
  81. # frappe.response["values"] = frappe.db.sql(scrub_custom_query(query, searchfield, txt))
  82. else:
  83. if isinstance(filters, dict):
  84. filters_items = filters.items()
  85. filters = []
  86. for f in filters_items:
  87. if isinstance(f[1], (list, tuple)):
  88. filters.append([doctype, f[0], f[1][0], f[1][1]])
  89. else:
  90. filters.append([doctype, f[0], "=", f[1]])
  91. if filters==None:
  92. filters = []
  93. or_filters = []
  94. # build from doctype
  95. if txt:
  96. search_fields = ["name"]
  97. if meta.title_field:
  98. search_fields.append(meta.title_field)
  99. if meta.search_fields:
  100. search_fields.extend(meta.get_search_fields())
  101. for f in search_fields:
  102. fmeta = meta.get_field(f.strip())
  103. if (doctype not in UNTRANSLATED_DOCTYPES) and (f == "name" or (fmeta and fmeta.fieldtype in ["Data", "Text", "Small Text", "Long Text",
  104. "Link", "Select", "Read Only", "Text Editor"])):
  105. or_filters.append([doctype, f.strip(), "like", "%{0}%".format(txt)])
  106. if meta.get("fields", {"fieldname":"enabled", "fieldtype":"Check"}):
  107. filters.append([doctype, "enabled", "=", 1])
  108. if meta.get("fields", {"fieldname":"disabled", "fieldtype":"Check"}):
  109. filters.append([doctype, "disabled", "!=", 1])
  110. # format a list of fields combining search fields and filter fields
  111. fields = get_std_fields_list(meta, searchfield or "name")
  112. if filter_fields:
  113. fields = list(set(fields + json.loads(filter_fields)))
  114. formatted_fields = ['`tab%s`.`%s`' % (meta.name, f.strip()) for f in fields]
  115. # find relevance as location of search term from the beginning of string `name`. used for sorting results.
  116. formatted_fields.append("""locate({_txt}, `tab{doctype}`.`name`) as `_relevance`""".format(
  117. _txt=frappe.db.escape((txt or "").replace("%", "").replace("@", "")), doctype=doctype))
  118. # In order_by, `idx` gets second priority, because it stores link count
  119. from frappe.model.db_query import get_order_by
  120. order_by_based_on_meta = get_order_by(doctype, meta)
  121. # 2 is the index of _relevance column
  122. order_by = "_relevance, {0}, `tab{1}`.idx desc".format(order_by_based_on_meta, doctype)
  123. ptype = 'select' if frappe.only_has_select_perm(doctype) else 'read'
  124. ignore_permissions = True if doctype == "DocType" else (cint(ignore_user_permissions) and has_permission(doctype, ptype=ptype))
  125. if doctype in UNTRANSLATED_DOCTYPES:
  126. page_length = None
  127. values = frappe.get_list(doctype,
  128. filters=filters,
  129. fields=formatted_fields,
  130. or_filters=or_filters,
  131. limit_start=start,
  132. limit_page_length=page_length,
  133. order_by=order_by,
  134. ignore_permissions=ignore_permissions,
  135. reference_doctype=reference_doctype,
  136. as_list=not as_dict,
  137. strict=False)
  138. if doctype in UNTRANSLATED_DOCTYPES:
  139. values = tuple([v for v in list(values) if re.search(re.escape(txt)+".*", (_(v.name) if as_dict else _(v[0])), re.IGNORECASE)])
  140. # remove _relevance from results
  141. if as_dict:
  142. for r in values:
  143. r.pop("_relevance")
  144. frappe.response["values"] = values
  145. else:
  146. frappe.response["values"] = [r[:-1] for r in values]
  147. def get_std_fields_list(meta, key):
  148. # get additional search fields
  149. sflist = ["name"]
  150. if meta.search_fields:
  151. for d in meta.search_fields.split(","):
  152. if d.strip() not in sflist:
  153. sflist.append(d.strip())
  154. if meta.title_field and meta.title_field not in sflist:
  155. sflist.append(meta.title_field)
  156. if key not in sflist:
  157. sflist.append(key)
  158. return sflist
  159. def build_for_autosuggest(res):
  160. results = []
  161. for r in res:
  162. out = {"value": r[0], "description": ", ".join(unique(cstr(d) for d in r if d)[1:])}
  163. results.append(out)
  164. return results
  165. def scrub_custom_query(query, key, txt):
  166. if '%(key)s' in query:
  167. query = query.replace('%(key)s', key)
  168. if '%s' in query:
  169. query = query.replace('%s', ((txt or '') + '%'))
  170. return query
  171. @wrapt.decorator
  172. def validate_and_sanitize_search_inputs(fn, instance, args, kwargs):
  173. kwargs.update(dict(zip(fn.__code__.co_varnames, args)))
  174. sanitize_searchfield(kwargs['searchfield'])
  175. kwargs['start'] = cint(kwargs['start'])
  176. kwargs['page_len'] = cint(kwargs['page_len'])
  177. if kwargs['doctype'] and not frappe.db.exists('DocType', kwargs['doctype']):
  178. return []
  179. return fn(**kwargs)
  180. @frappe.whitelist()
  181. def get_names_for_mentions(search_term):
  182. users_for_mentions = frappe.cache().get_value('users_for_mentions', get_users_for_mentions)
  183. user_groups = frappe.cache().get_value('user_groups', get_user_groups)
  184. filtered_mentions = []
  185. for mention_data in users_for_mentions + user_groups:
  186. if search_term.lower() not in mention_data.value.lower():
  187. continue
  188. mention_data['link'] = frappe.utils.get_url_to_form(
  189. 'User Group' if mention_data.get('is_group') else 'User Profile',
  190. mention_data['id']
  191. )
  192. filtered_mentions.append(mention_data)
  193. return sorted(filtered_mentions, key=lambda d: d['value'])
  194. def get_users_for_mentions():
  195. return frappe.get_all('User',
  196. fields=['name as id', 'full_name as value'],
  197. filters={
  198. 'name': ['not in', ('Administrator', 'Guest')],
  199. 'allowed_in_mentions': True,
  200. 'user_type': 'System User',
  201. })
  202. def get_user_groups():
  203. return frappe.get_all('User Group', fields=['name as id', 'name as value'], update={
  204. 'is_group': True
  205. })