Du kan inte välja fler än 25 ämnen Ämnen måste starta med en bokstav eller siffra, kan innehålla bindestreck ('-') och vara max 35 tecken långa.
 
 
 
 
 
 

446 rader
12 KiB

  1. # Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and Contributors
  2. # License: MIT. See LICENSE
  3. """
  4. Boot session from cache or build
  5. Session bootstraps info needed by common client side activities including
  6. permission, homepage, default variables, system defaults etc
  7. """
  8. import frappe, json
  9. from frappe import _
  10. import frappe.utils
  11. from frappe.utils import cint, cstr, get_assets_json
  12. import frappe.model.meta
  13. import frappe.defaults
  14. import frappe.translate
  15. import redis
  16. from urllib.parse import unquote
  17. from frappe.cache_manager import clear_user_cache
  18. from frappe.query_builder import Order, DocType
  19. from frappe.query_builder.utils import PseudoColumn
  20. from frappe.query_builder.functions import Now
  21. @frappe.whitelist()
  22. def clear():
  23. frappe.local.session_obj.update(force=True)
  24. frappe.local.db.commit()
  25. clear_user_cache(frappe.session.user)
  26. frappe.response['message'] = _("Cache Cleared")
  27. def clear_sessions(user=None, keep_current=False, device=None, force=False):
  28. '''Clear other sessions of the current user. Called at login / logout
  29. :param user: user name (default: current user)
  30. :param keep_current: keep current session (default: false)
  31. :param device: delete sessions of this device (default: desktop, mobile)
  32. :param force: triggered by the user (default false)
  33. '''
  34. reason = "Logged In From Another Session"
  35. if force:
  36. reason = "Force Logged out by the user"
  37. for sid in get_sessions_to_clear(user, keep_current, device):
  38. delete_session(sid, reason=reason)
  39. def get_sessions_to_clear(user=None, keep_current=False, device=None):
  40. '''Returns sessions of the current user. Called at login / logout
  41. :param user: user name (default: current user)
  42. :param keep_current: keep current session (default: false)
  43. :param device: delete sessions of this device (default: desktop, mobile)
  44. '''
  45. if not user:
  46. user = frappe.session.user
  47. if not device:
  48. device = ("desktop", "mobile")
  49. if not isinstance(device, (tuple, list)):
  50. device = (device,)
  51. offset = 0
  52. if user == frappe.session.user:
  53. simultaneous_sessions = frappe.db.get_value('User', user, 'simultaneous_sessions') or 1
  54. offset = simultaneous_sessions - 1
  55. session = DocType("Sessions")
  56. session_id = frappe.qb.from_(session).where((session.user == user) & (session.device.isin(device)))
  57. if keep_current:
  58. session_id = session_id.where(session.sid != frappe.session.sid)
  59. query = (
  60. session_id.select(session.sid)
  61. .offset(offset)
  62. .limit(100)
  63. .orderby(session.lastupdate, order=Order.desc)
  64. )
  65. return query.run(pluck=True)
  66. def delete_session(sid=None, user=None, reason="Session Expired"):
  67. from frappe.core.doctype.activity_log.feed import logout_feed
  68. frappe.cache().hdel("session", sid)
  69. frappe.cache().hdel("last_db_session_update", sid)
  70. if sid and not user:
  71. table = DocType("Sessions")
  72. user_details = frappe.qb.from_(table).where(
  73. table.sid == sid
  74. ).select(table.user).run(as_dict=True)
  75. if user_details: user = user_details[0].get("user")
  76. logout_feed(user, reason)
  77. frappe.db.delete("Sessions", {"sid": sid})
  78. frappe.db.commit()
  79. def clear_all_sessions(reason=None):
  80. """This effectively logs out all users"""
  81. frappe.only_for("Administrator")
  82. if not reason: reason = "Deleted All Active Session"
  83. for sid in frappe.qb.from_("Sessions").select("sid").run(pluck=True):
  84. delete_session(sid, reason=reason)
  85. def get_expired_sessions():
  86. '''Returns list of expired sessions'''
  87. sessions = DocType("Sessions")
  88. expired = []
  89. for device in ("desktop", "mobile"):
  90. expired.extend(
  91. frappe.db.get_values(
  92. sessions,
  93. filters=(
  94. PseudoColumn(f"({Now()} - {sessions.lastupdate.get_sql()})")
  95. > get_expiry_period_for_query(device)
  96. )
  97. & (sessions.device == device),
  98. fieldname="sid",
  99. order_by=None,
  100. pluck=True,
  101. )
  102. )
  103. return expired
  104. def clear_expired_sessions():
  105. """This function is meant to be called from scheduler"""
  106. for sid in get_expired_sessions():
  107. delete_session(sid, reason="Session Expired")
  108. def get():
  109. """get session boot info"""
  110. from frappe.boot import get_bootinfo, get_unseen_notes
  111. from frappe.utils.change_log import get_change_log
  112. bootinfo = None
  113. if not getattr(frappe.conf,'disable_session_cache', None):
  114. # check if cache exists
  115. bootinfo = frappe.cache().hget("bootinfo", frappe.session.user)
  116. if bootinfo:
  117. bootinfo['from_cache'] = 1
  118. bootinfo["user"]["recent"] = json.dumps(\
  119. frappe.cache().hget("user_recent", frappe.session.user))
  120. if not bootinfo:
  121. # if not create it
  122. bootinfo = get_bootinfo()
  123. frappe.cache().hset("bootinfo", frappe.session.user, bootinfo)
  124. try:
  125. frappe.cache().ping()
  126. except redis.exceptions.ConnectionError:
  127. message = _("Redis cache server not running. Please contact Administrator / Tech support")
  128. if 'messages' in bootinfo:
  129. bootinfo['messages'].append(message)
  130. else:
  131. bootinfo['messages'] = [message]
  132. # check only when clear cache is done, and don't cache this
  133. if frappe.local.request:
  134. bootinfo["change_log"] = get_change_log()
  135. bootinfo["metadata_version"] = frappe.cache().get_value("metadata_version")
  136. if not bootinfo["metadata_version"]:
  137. bootinfo["metadata_version"] = frappe.reset_metadata_version()
  138. bootinfo.notes = get_unseen_notes()
  139. bootinfo.assets_json = get_assets_json()
  140. for hook in frappe.get_hooks("extend_bootinfo"):
  141. frappe.get_attr(hook)(bootinfo=bootinfo)
  142. bootinfo["lang"] = frappe.translate.get_user_lang()
  143. bootinfo["disable_async"] = frappe.conf.disable_async
  144. bootinfo["setup_complete"] = cint(frappe.db.get_single_value('System Settings', 'setup_complete'))
  145. bootinfo["is_first_startup"] = cint(frappe.db.get_single_value('System Settings', 'is_first_startup'))
  146. bootinfo['desk_theme'] = frappe.db.get_value("User", frappe.session.user, "desk_theme") or 'Light'
  147. return bootinfo
  148. @frappe.whitelist()
  149. def get_boot_assets_json():
  150. return get_assets_json()
  151. def get_csrf_token():
  152. if not frappe.local.session.data.csrf_token:
  153. generate_csrf_token()
  154. return frappe.local.session.data.csrf_token
  155. def generate_csrf_token():
  156. frappe.local.session.data.csrf_token = frappe.generate_hash()
  157. if not frappe.flags.in_test:
  158. frappe.local.session_obj.update(force=True)
  159. class Session:
  160. def __init__(self, user, resume=False, full_name=None, user_type=None):
  161. self.sid = cstr(frappe.form_dict.get('sid') or
  162. unquote(frappe.request.cookies.get('sid', 'Guest')))
  163. self.user = user
  164. self.device = frappe.form_dict.get("device") or "desktop"
  165. self.user_type = user_type
  166. self.full_name = full_name
  167. self.data = frappe._dict({'data': frappe._dict({})})
  168. self.time_diff = None
  169. # set local session
  170. frappe.local.session = self.data
  171. if resume:
  172. self.resume()
  173. else:
  174. if self.user:
  175. self.start()
  176. def start(self):
  177. """start a new session"""
  178. # generate sid
  179. if self.user=='Guest':
  180. sid = 'Guest'
  181. else:
  182. sid = frappe.generate_hash()
  183. self.data.user = self.user
  184. self.data.sid = sid
  185. self.data.data.user = self.user
  186. self.data.data.session_ip = frappe.local.request_ip
  187. if self.user != "Guest":
  188. self.data.data.update({
  189. "last_updated": frappe.utils.now(),
  190. "session_expiry": get_expiry_period(self.device),
  191. "full_name": self.full_name,
  192. "user_type": self.user_type,
  193. "device": self.device,
  194. "session_country": get_geo_ip_country(frappe.local.request_ip) if frappe.local.request_ip else None,
  195. })
  196. # insert session
  197. if self.user!="Guest":
  198. self.insert_session_record()
  199. # update user
  200. user = frappe.get_doc("User", self.data['user'])
  201. frappe.db.sql("""UPDATE `tabUser`
  202. SET
  203. last_login = %(now)s,
  204. last_ip = %(ip)s,
  205. last_active = %(now)s
  206. WHERE name=%(name)s""", {
  207. 'now': frappe.utils.now(),
  208. 'ip': frappe.local.request_ip,
  209. 'name': self.data['user']
  210. })
  211. user.run_notifications("before_change")
  212. user.run_notifications("on_update")
  213. frappe.db.commit()
  214. def insert_session_record(self):
  215. frappe.db.sql("""insert into `tabSessions`
  216. (`sessiondata`, `user`, `lastupdate`, `sid`, `status`, `device`)
  217. values (%s , %s, NOW(), %s, 'Active', %s)""",
  218. (str(self.data['data']), self.data['user'], self.data['sid'], self.device))
  219. # also add to memcache
  220. frappe.cache().hset("session", self.data.sid, self.data)
  221. def resume(self):
  222. """non-login request: load a session"""
  223. import frappe
  224. from frappe.auth import validate_ip_address
  225. data = self.get_session_record()
  226. if data:
  227. self.data.update({'data': data, 'user':data.user, 'sid': self.sid})
  228. self.user = data.user
  229. validate_ip_address(self.user)
  230. self.device = data.device
  231. else:
  232. self.start_as_guest()
  233. if self.sid != "Guest":
  234. frappe.local.user_lang = frappe.translate.get_user_lang(self.data.user)
  235. frappe.local.lang = frappe.local.user_lang
  236. def get_session_record(self):
  237. """get session record, or return the standard Guest Record"""
  238. from frappe.auth import clear_cookies
  239. r = self.get_session_data()
  240. if not r:
  241. frappe.response["session_expired"] = 1
  242. clear_cookies()
  243. self.sid = "Guest"
  244. r = self.get_session_data()
  245. return r
  246. def get_session_data(self):
  247. if self.sid=="Guest":
  248. return frappe._dict({"user":"Guest"})
  249. data = self.get_session_data_from_cache()
  250. if not data:
  251. data = self.get_session_data_from_db()
  252. return data
  253. def get_session_data_from_cache(self):
  254. data = frappe.cache().hget("session", self.sid)
  255. if data:
  256. data = frappe._dict(data)
  257. session_data = data.get("data", {})
  258. # set user for correct timezone
  259. self.time_diff = frappe.utils.time_diff_in_seconds(frappe.utils.now(),
  260. session_data.get("last_updated"))
  261. expiry = get_expiry_in_seconds(session_data.get("session_expiry"))
  262. if self.time_diff > expiry:
  263. self._delete_session()
  264. data = None
  265. return data and data.data
  266. def get_session_data_from_db(self):
  267. sessions = DocType("Sessions")
  268. self.device = frappe.db.get_value(
  269. sessions, filters=sessions.sid == self.sid, fieldname="device", order_by=None,
  270. ) or "desktop"
  271. rec = frappe.db.get_values(
  272. sessions,
  273. filters=(sessions.sid == self.sid)
  274. & (
  275. PseudoColumn(f"({Now()} - {sessions.lastupdate.get_sql()})")
  276. < get_expiry_period_for_query(self.device)
  277. ),
  278. fieldname=["user", "sessiondata"],
  279. order_by=None,
  280. )
  281. if rec:
  282. data = frappe._dict(frappe.safe_eval(rec and rec[0][1] or '{}'))
  283. data.user = rec[0][0]
  284. else:
  285. self._delete_session()
  286. data = None
  287. return data
  288. def _delete_session(self):
  289. delete_session(self.sid, reason="Session Expired")
  290. def start_as_guest(self):
  291. """all guests share the same 'Guest' session"""
  292. self.user = "Guest"
  293. self.start()
  294. def update(self, force=False):
  295. """extend session expiry"""
  296. if (frappe.session['user'] == "Guest" or frappe.form_dict.cmd=="logout"):
  297. return
  298. now = frappe.utils.now()
  299. self.data['data']['last_updated'] = now
  300. self.data['data']['lang'] = str(frappe.lang)
  301. # update session in db
  302. last_updated = frappe.cache().hget("last_db_session_update", self.sid)
  303. time_diff = frappe.utils.time_diff_in_seconds(now, last_updated) if last_updated else None
  304. # database persistence is secondary, don't update it too often
  305. updated_in_db = False
  306. if force or (time_diff is None) or (time_diff > 600):
  307. # update sessions table
  308. frappe.db.sql("""update `tabSessions` set sessiondata=%s,
  309. lastupdate=NOW() where sid=%s""" , (str(self.data['data']),
  310. self.data['sid']))
  311. # update last active in user table
  312. frappe.db.sql("""update `tabUser` set last_active=%(now)s where name=%(name)s""", {
  313. "now": now,
  314. "name": frappe.session.user
  315. })
  316. frappe.db.commit()
  317. frappe.cache().hset("last_db_session_update", self.sid, now)
  318. updated_in_db = True
  319. # set in memcache
  320. frappe.cache().hset("session", self.sid, self.data)
  321. return updated_in_db
  322. def get_expiry_period_for_query(device=None):
  323. if frappe.db.db_type == 'postgres':
  324. return get_expiry_period(device)
  325. else:
  326. return get_expiry_in_seconds(device=device)
  327. def get_expiry_in_seconds(expiry=None, device=None):
  328. if not expiry:
  329. expiry = get_expiry_period(device)
  330. parts = expiry.split(":")
  331. return (cint(parts[0]) * 3600) + (cint(parts[1]) * 60) + cint(parts[2])
  332. def get_expiry_period(device="desktop"):
  333. if device=="mobile":
  334. key = "session_expiry_mobile"
  335. default = "720:00:00"
  336. else:
  337. key = "session_expiry"
  338. default = "06:00:00"
  339. exp_sec = frappe.defaults.get_global_default(key) or default
  340. # incase seconds is missing
  341. if len(exp_sec.split(':')) == 2:
  342. exp_sec = exp_sec + ':00'
  343. return exp_sec
  344. def get_geo_from_ip(ip_addr):
  345. try:
  346. from geolite2 import geolite2
  347. with geolite2 as f:
  348. reader = f.reader()
  349. data = reader.get(ip_addr)
  350. return frappe._dict(data)
  351. except ImportError:
  352. return
  353. except ValueError:
  354. return
  355. except TypeError:
  356. return
  357. def get_geo_ip_country(ip_addr):
  358. match = get_geo_from_ip(ip_addr)
  359. if match:
  360. return match.country