user.rb 30 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111
  1. # Copyright (C) 2012-2024 Zammad Foundation, https://zammad-foundation.org/
  2. class User < ApplicationModel
  3. include CanBeImported
  4. include HasActivityStreamLog
  5. include ChecksClientNotification
  6. include HasHistory
  7. include HasSearchIndexBackend
  8. include CanSelector
  9. include CanCsvImport
  10. include ChecksHtmlSanitized
  11. include HasGroups
  12. include HasRoles
  13. include HasObjectManagerAttributes
  14. include HasTaskbars
  15. include HasTwoFactor
  16. include CanSelector
  17. include CanPerformChanges
  18. include User::Assets
  19. include User::Avatar
  20. include User::Search
  21. include User::SearchIndex
  22. include User::TouchesOrganization
  23. include User::TriggersSubscriptions
  24. include User::PerformsGeoLookup
  25. include User::UpdatesTicketOrganization
  26. include User::OutOfOffice
  27. include User::Permissions
  28. has_and_belongs_to_many :organizations, after_add: %i[cache_update create_organization_add_history], after_remove: %i[cache_update create_organization_remove_history], class_name: 'Organization'
  29. has_and_belongs_to_many :overviews, dependent: :nullify
  30. has_many :tokens, after_add: :cache_update, after_remove: :cache_update, dependent: :destroy
  31. has_many :authorizations, after_add: :cache_update, after_remove: :cache_update, dependent: :destroy
  32. has_many :online_notifications, dependent: :destroy
  33. has_many :taskbars, dependent: :destroy
  34. has_many :user_devices, dependent: :destroy
  35. has_one :chat_agent_created_by, class_name: 'Chat::Agent', foreign_key: :created_by_id, dependent: :destroy, inverse_of: :created_by
  36. has_one :chat_agent_updated_by, class_name: 'Chat::Agent', foreign_key: :updated_by_id, dependent: :destroy, inverse_of: :updated_by
  37. has_many :chat_sessions, class_name: 'Chat::Session', dependent: :destroy
  38. has_many :mentions, dependent: :destroy
  39. has_many :cti_caller_ids, class_name: 'Cti::CallerId', dependent: :destroy
  40. has_many :customer_tickets, class_name: 'Ticket', foreign_key: :customer_id, dependent: :destroy, inverse_of: :customer
  41. has_many :owner_tickets, class_name: 'Ticket', foreign_key: :owner_id, inverse_of: :owner
  42. has_many :overview_sortings, dependent: :destroy
  43. has_many :created_recent_views, class_name: 'RecentView', foreign_key: :created_by_id, dependent: :destroy, inverse_of: :created_by
  44. has_many :data_privacy_tasks, as: :deletable
  45. belongs_to :organization, inverse_of: :members, optional: true
  46. before_validation :check_name, :check_email, :check_login, :ensure_password, :ensure_roles, :ensure_organizations, :ensure_organizations_limit
  47. before_validation :check_mail_delivery_failed, on: :update
  48. before_save :ensure_notification_preferences, if: :reset_notification_config_before_save
  49. before_create :validate_preferences, :domain_based_assignment, :set_locale
  50. before_update :validate_preferences, :reset_login_failed_after_password_change, :validate_agent_limit_by_attributes, :last_admin_check_by_attribute
  51. before_destroy :destroy_longer_required_objects, :destroy_move_dependency_ownership
  52. after_commit :update_caller_id
  53. validate :ensure_identifier, :ensure_email
  54. validate :ensure_uniq_email, unless: :skip_ensure_uniq_email
  55. available_perform_change_actions :data_privacy_deletion_task, :attribute_updates
  56. # workflow checks should run after before_create and before_update callbacks
  57. # the transaction dispatcher must be run after the workflow checks!
  58. include ChecksCoreWorkflow
  59. include HasTransactionDispatcher
  60. core_workflow_screens 'create', 'edit', 'invite_agent'
  61. core_workflow_admin_screens 'create', 'edit'
  62. store :preferences
  63. association_attributes_ignored :online_notifications,
  64. :templates,
  65. :taskbars,
  66. :user_devices,
  67. :chat_sessions,
  68. :cti_caller_ids,
  69. :text_modules,
  70. :customer_tickets,
  71. :owner_tickets,
  72. :created_recent_views,
  73. :chat_agents,
  74. :data_privacy_tasks,
  75. :overviews,
  76. :mentions
  77. activity_stream_permission 'admin.user'
  78. activity_stream_attributes_ignored :last_login,
  79. :login_failed,
  80. :image,
  81. :image_source,
  82. :preferences
  83. history_attributes_ignored :password,
  84. :last_login,
  85. :image,
  86. :image_source,
  87. :preferences
  88. search_index_attributes_ignored :password,
  89. :image,
  90. :image_source,
  91. :source,
  92. :login_failed
  93. csv_object_ids_ignored 1
  94. csv_attributes_ignored :password,
  95. :login_failed,
  96. :source,
  97. :image_source,
  98. :image,
  99. :authorizations,
  100. :groups,
  101. :user_groups
  102. validates :note, length: { maximum: 5000 }
  103. sanitized_html :note, no_images: true
  104. def ignore_search_indexing?(_action)
  105. # ignore internal user
  106. return true if id == 1
  107. false
  108. end
  109. =begin
  110. fullname of user
  111. user = User.find(123)
  112. result = user.fullname
  113. returns
  114. result = "Bob Smith"
  115. =end
  116. def fullname(email_fallback: true, recipient_line: false)
  117. name = "#{firstname} #{lastname}".strip
  118. if name.blank? && email.present? && email_fallback
  119. return email
  120. elsif recipient_line
  121. begin
  122. return Channel::EmailBuild.recipient_line(name, email)
  123. rescue
  124. return email
  125. end
  126. end
  127. return name if name.present?
  128. %w[phone mobile].each do |item|
  129. next if self[item].blank?
  130. return self[item]
  131. end
  132. name
  133. end
  134. =begin
  135. longname of user
  136. user = User.find(123)
  137. result = user.longname
  138. returns
  139. result = "Bob Smith"
  140. or with org
  141. result = "Bob Smith (Org ABC)"
  142. =end
  143. def longname
  144. name = fullname
  145. if organization_id
  146. organization = Organization.lookup(id: organization_id)
  147. if organization
  148. name += " (#{organization.name})"
  149. end
  150. end
  151. name
  152. end
  153. =begin
  154. check if user is in role
  155. user = User.find(123)
  156. result = user.role?('Customer')
  157. result = user.role?(['Agent', 'Admin'])
  158. returns
  159. result = true|false
  160. =end
  161. def role?(role_name)
  162. roles.where(name: role_name).any?
  163. end
  164. =begin
  165. get users activity stream
  166. user = User.find(123)
  167. result = user.activity_stream(20)
  168. returns
  169. result = [
  170. {
  171. id: 2,
  172. o_id: 2,
  173. created_by_id: 3,
  174. created_at: '2013-09-28 00:57:21',
  175. object: "User",
  176. type: "created",
  177. },
  178. {
  179. id: 2,
  180. o_id: 2,
  181. created_by_id: 3,
  182. created_at: '2013-09-28 00:59:21',
  183. object: "User",
  184. type: "updated",
  185. },
  186. ]
  187. =end
  188. def activity_stream(limit, fulldata = false)
  189. stream = ActivityStream.list(self, limit)
  190. return stream if !fulldata
  191. # get related objects
  192. assets = {}
  193. stream.each do |item|
  194. assets = item.assets(assets)
  195. end
  196. {
  197. stream: stream,
  198. assets: assets,
  199. }
  200. end
  201. =begin
  202. tries to find the matching instance by the given identifier. Currently email and login is supported.
  203. user = User.indentify('User123')
  204. # or
  205. user = User.indentify('user-123@example.com')
  206. returns
  207. # User instance
  208. user.login # 'user123'
  209. =end
  210. def self.identify(identifier)
  211. return if identifier.blank?
  212. # try to find user based on login
  213. user = User.find_by(login: identifier.downcase)
  214. return user if user
  215. # try second lookup with email
  216. User.find_by(email: identifier.downcase)
  217. end
  218. =begin
  219. create user from from omni auth hash
  220. result = User.create_from_hash!(hash)
  221. returns
  222. result = user_model # user model if create was successfully
  223. =end
  224. def self.create_from_hash!(hash)
  225. url = ''
  226. hash['info']['urls']&.each_value do |local_url|
  227. next if local_url.blank?
  228. url = local_url
  229. end
  230. begin
  231. data = {
  232. login: hash['info']['nickname'] || hash['uid'],
  233. firstname: hash['info']['name'] || hash['info']['display_name'],
  234. email: hash['info']['email'],
  235. image_source: hash['info']['image'],
  236. web: url,
  237. address: hash['info']['location'],
  238. note: hash['info']['description'],
  239. source: hash['provider'],
  240. role_ids: Role.signup_role_ids,
  241. updated_by_id: 1,
  242. created_by_id: 1,
  243. }
  244. if hash['info']['first_name'].present? && hash['info']['last_name'].present?
  245. data[:firstname] = hash['info']['first_name']
  246. data[:lastname] = hash['info']['last_name']
  247. end
  248. create!(data)
  249. rescue => e
  250. logger.error e
  251. raise Exceptions::UnprocessableEntity, e.message
  252. end
  253. end
  254. # Find a user by mobile number, either directly or by number variants stored in the Cti::CallerIds.
  255. def self.by_mobile(number:)
  256. direct_lookup = User.where(mobile: number).reorder(:updated_at).first
  257. return direct_lookup if direct_lookup
  258. cti_lookup = Cti::CallerId.lookup(number.delete('+')).find { |id| id.level == 'known' && id.object == 'User' }
  259. User.find_by(id: cti_lookup.o_id) if cti_lookup
  260. end
  261. =begin
  262. generate new token for reset password
  263. result = User.password_reset_new_token(username)
  264. returns
  265. result = {
  266. token: token,
  267. user: user,
  268. }
  269. =end
  270. def self.password_reset_new_token(username)
  271. return if username.blank?
  272. # try to find user based on login
  273. user = User.find_by(login: username.downcase.strip, active: true)
  274. # try second lookup with email
  275. user ||= User.find_by(email: username.downcase.strip, active: true)
  276. return if !user || !user.email
  277. # Discard any possible previous tokens for safety reasons.
  278. Token.where(action: 'PasswordReset', user_id: user.id).destroy_all
  279. {
  280. token: Token.create(action: 'PasswordReset', user_id: user.id, persistent: false),
  281. user: user,
  282. }
  283. end
  284. =begin
  285. returns the User instance for a given password token if found
  286. result = User.by_reset_token(token)
  287. returns
  288. result = user_model # user_model if token was verified
  289. =end
  290. def self.by_reset_token(token)
  291. Token.check(action: 'PasswordReset', token: token)
  292. end
  293. =begin
  294. reset password with token and set new password
  295. result = User.password_reset_via_token(token,password)
  296. returns
  297. result = user_model # user_model if token was verified
  298. =end
  299. def self.password_reset_via_token(token, password)
  300. # check token
  301. user = by_reset_token(token)
  302. return if !user
  303. # reset password
  304. user.update!(password: password, verified: true)
  305. # delete token
  306. Token.find_by(action: 'PasswordReset', token: token).destroy
  307. user
  308. end
  309. def self.admin_password_auth_new_token(username)
  310. return if username.blank?
  311. # try to find user based on login
  312. user = User.find_by(login: username.downcase.strip, active: true)
  313. # try second lookup with email
  314. user ||= User.find_by(email: username.downcase.strip, active: true)
  315. return if !user || !user.email
  316. return if !user.permissions?('admin.*')
  317. # Discard any possible previous tokens for safety reasons.
  318. Token.where(action: 'AdminAuth', user_id: user.id).destroy_all
  319. {
  320. token: Token.create(action: 'AdminAuth', user_id: user.id, persistent: false),
  321. user: user,
  322. }
  323. end
  324. def self.admin_password_auth_via_token(token)
  325. user = Token.check(action: 'AdminAuth', token: token)
  326. return if !user
  327. Token.find_by(action: 'AdminAuth', token: token).destroy
  328. user
  329. end
  330. =begin
  331. update last login date and reset login_failed (is automatically done by auth and sso backend)
  332. user = User.find(123)
  333. result = user.update_last_login
  334. returns
  335. result = new_user_model
  336. =end
  337. def update_last_login
  338. # reduce DB/ES load by updating last_login every 10 minutes only
  339. if !last_login || last_login < 10.minutes.ago
  340. self.last_login = Time.zone.now
  341. end
  342. # reset login failed
  343. self.login_failed = 0
  344. save
  345. end
  346. =begin
  347. generate new token for signup
  348. result = User.signup_new_token(user) # or email
  349. returns
  350. result = {
  351. token: token,
  352. user: user,
  353. }
  354. =end
  355. def self.signup_new_token(user)
  356. return if !user
  357. return if !user.email
  358. # Discard any possible previous tokens for safety reasons.
  359. Token.where(action: 'Signup', user_id: user.id).destroy_all
  360. # generate token
  361. token = Token.create(action: 'Signup', user_id: user.id)
  362. {
  363. token: token,
  364. user: user,
  365. }
  366. end
  367. =begin
  368. verify signup with token
  369. result = User.signup_verify_via_token(token, user)
  370. returns
  371. result = user_model # user_model if token was verified
  372. =end
  373. def self.signup_verify_via_token(token, user = nil)
  374. # check token
  375. local_user = Token.check(action: 'Signup', token: token)
  376. return if !local_user
  377. # if requested user is different to current user
  378. return if user && local_user.id != user.id
  379. # set verified
  380. local_user.update!(verified: true)
  381. # delete token
  382. Token.find_by(action: 'Signup', token: token).destroy
  383. local_user
  384. end
  385. =begin
  386. merge two users to one
  387. user = User.find(123)
  388. result = user.merge(user_id_of_duplicate_user)
  389. returns
  390. result = new_user_model
  391. =end
  392. def merge(user_id_of_duplicate_user)
  393. # Raise an exception if the user is not found (?)
  394. #
  395. # (This line used to contain a useless variable assignment,
  396. # and was changed to satisfy the linter.
  397. # We're not certain of its original intention,
  398. # so the User.find call has been kept
  399. # to prevent any unexpected regressions.)
  400. User.find(user_id_of_duplicate_user)
  401. # mentions can not merged easily because the new user could have mentioned
  402. # the same ticket so we delete duplicates beforehand
  403. Mention.where(user_id: user_id_of_duplicate_user).find_each do |mention|
  404. if Mention.exists?(mentionable: mention.mentionable, user_id: id)
  405. mention.destroy
  406. else
  407. mention.update(user_id: id)
  408. end
  409. end
  410. # merge missing attributes
  411. Models.merge('User', id, user_id_of_duplicate_user)
  412. true
  413. end
  414. =begin
  415. list of active users in role
  416. result = User.of_role('Agent', group_ids)
  417. result = User.of_role(['Agent', 'Admin'])
  418. returns
  419. result = [user1, user2]
  420. =end
  421. def self.of_role(role, group_ids = nil)
  422. roles_ids = Role.where(active: true, name: role).map(&:id)
  423. if !group_ids
  424. return User.where(active: true).joins(:roles_users).where('roles_users.role_id' => roles_ids).reorder('users.updated_at DESC')
  425. end
  426. User.where(active: true)
  427. .joins(:roles_users)
  428. .joins(:users_groups)
  429. .where('roles_users.role_id IN (?) AND users_groups.group_ids IN (?)', roles_ids, group_ids).reorder('users.updated_at DESC')
  430. end
  431. # Reset agent notification preferences
  432. # Non-agent cannot receive notifications, thus notifications reset
  433. #
  434. # @option user [User] to reset preferences
  435. def self.reset_notifications_preferences!(user)
  436. return if !user.permissions? 'ticket.agent'
  437. user.fill_notification_config_preferences
  438. user.save!
  439. end
  440. =begin
  441. try to find correct name
  442. [firstname, lastname] = User.name_guess('Some Name', 'some.name@example.com')
  443. =end
  444. def self.name_guess(string, email = nil)
  445. return if string.blank? && email.blank?
  446. string.strip!
  447. firstname = ''
  448. lastname = ''
  449. # "Lastname, Firstname"
  450. if string.match?(',')
  451. name = string.split(', ', 2)
  452. if name.count == 2
  453. if name[0].present?
  454. lastname = name[0].strip
  455. end
  456. if name[1].present?
  457. firstname = name[1].strip
  458. end
  459. return [firstname, lastname] if firstname.present? || lastname.present?
  460. end
  461. end
  462. # "Firstname Lastname"
  463. if string =~ %r{^(((Dr\.|Prof\.)[[:space:]]|).+?)[[:space:]](.+?)$}i
  464. if $1.present?
  465. firstname = $1.strip
  466. end
  467. if $4.present?
  468. lastname = $4.strip
  469. end
  470. return [firstname, lastname] if firstname.present? || lastname.present?
  471. end
  472. # -no name- "firstname.lastname@example.com"
  473. if string.blank? && email.present?
  474. scan = email.scan(%r{^(.+?)\.(.+?)@.+?$})
  475. if scan[0].present?
  476. if scan[0][0].present?
  477. firstname = scan[0][0].strip
  478. end
  479. if scan[0][1].present?
  480. lastname = scan[0][1].strip
  481. end
  482. return [firstname, lastname] if firstname.present? || lastname.present?
  483. end
  484. end
  485. nil
  486. end
  487. def no_name?
  488. firstname.blank? && lastname.blank?
  489. end
  490. # get locale identifier of user or system if user's own is not set
  491. def locale
  492. preferences.fetch(:locale) { Locale.default }
  493. end
  494. attr_accessor :skip_ensure_uniq_email
  495. def shared_organizations?
  496. all_organizations.exists? shared: true
  497. end
  498. def all_organizations
  499. Organization.where(id: all_organization_ids)
  500. end
  501. def all_organization_ids
  502. ([organization_id] + organization_ids).uniq
  503. end
  504. def organization_id?(organization_id)
  505. all_organization_ids.include?(organization_id)
  506. end
  507. def create_organization_add_history(org)
  508. organization_history_log(org, 'added')
  509. end
  510. def create_organization_remove_history(org)
  511. organization_history_log(org, 'removed')
  512. end
  513. def fill_notification_config_preferences
  514. preferences[:notification_config] ||= {}
  515. preferences[:notification_config][:matrix] = Setting.get('ticket_agent_default_notifications')
  516. end
  517. def mail_delivery_failed_blocked_days
  518. return 0 if !preferences[:mail_delivery_failed]
  519. return 0 if preferences[:mail_delivery_failed_data].blank?
  520. # Blocked for 60 full days; see #4459.
  521. remaining_days = (preferences[:mail_delivery_failed_data].to_date - Time.zone.now.to_date).to_i + 61
  522. return remaining_days if remaining_days.positive?
  523. reset_mail_delivery_failed
  524. 0
  525. end
  526. def reset_mail_delivery_failed
  527. preferences[:mail_delivery_failed] = false
  528. preferences[:mail_delivery_failed_data] = nil
  529. save!
  530. end
  531. private
  532. def organization_history_log(org, type)
  533. return if id.blank?
  534. attributes = {
  535. history_attribute: 'organization_ids',
  536. id_to: org.id,
  537. value_to: org.name
  538. }
  539. history_log(type, id, attributes)
  540. end
  541. def check_name
  542. self.firstname = sanitize_name(firstname)
  543. self.lastname = sanitize_name(lastname)
  544. return if firstname.present? && lastname.present?
  545. if (firstname.blank? && lastname.present?) || (firstname.present? && lastname.blank?)
  546. used_name = firstname.presence || lastname
  547. (local_firstname, local_lastname) = User.name_guess(used_name, email)
  548. elsif firstname.blank? && lastname.blank? && email.present?
  549. (local_firstname, local_lastname) = User.name_guess('', email)
  550. end
  551. check_name_apply(:firstname, local_firstname)
  552. check_name_apply(:lastname, local_lastname)
  553. end
  554. def sanitize_name(value)
  555. result = value&.strip
  556. return result if result.blank?
  557. result.split(%r{\s}).map { |v| strip_uri(v) }.join("\s")
  558. end
  559. def strip_uri(value)
  560. uri = URI.parse(value)
  561. return value if !uri || uri.scheme.blank? || uri.hostname.blank?
  562. # Strip the scheme from the URI.
  563. uri.hostname + uri.path
  564. rescue
  565. value
  566. end
  567. def check_name_apply(identifier, input)
  568. self[identifier] = input if input.present?
  569. self[identifier].capitalize! if self[identifier]&.match? %r{^([[:upper:]]+|[[:lower:]]+)$}
  570. end
  571. def check_email
  572. return if Setting.get('import_mode')
  573. return if email.blank?
  574. # https://bugs.chromium.org/p/chromium/issues/detail?id=410937
  575. self.email = EmailHelper::Idn.to_unicode(email).downcase.strip
  576. end
  577. def ensure_email
  578. return if Setting.get('import_mode')
  579. return if email.blank?
  580. return if id == 1
  581. email_address_validation = EmailAddressValidation.new(email)
  582. return if email_address_validation.valid?
  583. errors.add :base, __("Invalid email '%{email}'"), email: email
  584. end
  585. def check_login
  586. # use email as login if not given
  587. if login.blank?
  588. self.login = email
  589. end
  590. # if email has changed, login is old email, change also login
  591. if email_changed? && email_was == login
  592. self.login = email
  593. end
  594. # generate auto login
  595. if login.blank?
  596. self.login = "auto-#{SecureRandom.uuid}"
  597. end
  598. # check if login already exists
  599. base_login = login.downcase.strip
  600. alternatives = [nil] + Array(1..20) + [ SecureRandom.uuid ]
  601. alternatives.each do |suffix|
  602. self.login = "#{base_login}#{suffix}"
  603. exists = User.find_by(login: login)
  604. return true if !exists || exists.id == id
  605. end
  606. raise Exceptions::UnprocessableEntity, "Invalid user login generation for login #{login}!"
  607. end
  608. def check_mail_delivery_failed
  609. return if email_change.blank?
  610. preferences.delete(:mail_delivery_failed)
  611. end
  612. def ensure_roles
  613. return if role_ids.present?
  614. self.role_ids = Role.signup_role_ids
  615. end
  616. def ensure_identifier
  617. return if login.present? && !login.start_with?('auto-')
  618. return if [email, firstname, lastname, phone, mobile].any?(&:present?)
  619. errors.add :base, __('At least one identifier (firstname, lastname, phone, mobile or email) for user is required.')
  620. end
  621. def ensure_uniq_email
  622. return if Setting.get('user_email_multiple_use')
  623. return if Setting.get('import_mode')
  624. return if email.blank?
  625. return if !email_changed?
  626. return if !User.exists?(email: email.downcase.strip)
  627. errors.add :base, __("Email address '%{email}' is already used for another user."), email: email.downcase.strip
  628. end
  629. def ensure_organizations
  630. return if organization_ids.blank?
  631. return if organization_id.present?
  632. errors.add :base, __('Secondary organizations are only allowed when the primary organization is given.')
  633. end
  634. def ensure_organizations_limit
  635. return if organization_ids.size <= 250
  636. errors.add :base, __('More than 250 secondary organizations are not allowed.')
  637. end
  638. def validate_roles(role)
  639. return true if !role_ids # we need role_ids for checking in role_ids below, in this method
  640. return true if role.preferences[:not].blank?
  641. role.preferences[:not].each do |local_role_name|
  642. local_role = Role.lookup(name: local_role_name)
  643. next if !local_role
  644. next if role_ids.exclude?(local_role.id)
  645. raise "Role #{role.name} conflicts with #{local_role.name}"
  646. end
  647. true
  648. end
  649. def validate_preferences
  650. return true if !changes
  651. return true if !changes['preferences']
  652. return true if preferences.blank?
  653. return true if !preferences[:notification_sound]
  654. return true if !preferences[:notification_sound][:enabled]
  655. case preferences[:notification_sound][:enabled]
  656. when 'true'
  657. preferences[:notification_sound][:enabled] = true
  658. when 'false'
  659. preferences[:notification_sound][:enabled] = false
  660. end
  661. class_name = preferences[:notification_sound][:enabled].class.to_s
  662. raise Exceptions::UnprocessableEntity, "preferences.notification_sound.enabled needs to be an boolean, but it was a #{class_name}" if class_name != 'TrueClass' && class_name != 'FalseClass'
  663. true
  664. end
  665. def ensure_notification_preferences
  666. fill_notification_config_preferences
  667. self.reset_notification_config_before_save = false
  668. end
  669. =begin
  670. checks if the current user is the last one with admin permissions.
  671. Raises
  672. raise 'At least one user need to have admin permissions'
  673. =end
  674. def last_admin_check_by_attribute
  675. return true if !will_save_change_to_attribute?('active')
  676. return true if active != false
  677. return true if !permissions?(['admin', 'admin.user'])
  678. raise Exceptions::UnprocessableEntity, __('At least one user needs to have admin permissions.') if last_admin_check_admin_count < 1
  679. true
  680. end
  681. def last_admin_check_by_role(role)
  682. return true if Setting.get('import_mode')
  683. return true if !role.with_permission?(['admin', 'admin.user'])
  684. raise Exceptions::UnprocessableEntity, __('At least one user needs to have admin permissions.') if last_admin_check_admin_count < 1
  685. true
  686. end
  687. def last_admin_check_admin_count
  688. admin_role_ids = Role.joins(:permissions).where(permissions: { name: ['admin', 'admin.user'], active: true }, roles: { active: true }).pluck(:id)
  689. User.joins(:roles).where(roles: { id: admin_role_ids }, users: { active: true }).distinct.count - 1
  690. end
  691. def validate_agent_limit_by_attributes
  692. return true if Setting.get('system_agent_limit').blank?
  693. return true if !will_save_change_to_attribute?('active')
  694. return true if active != true
  695. return true if !permissions?('ticket.agent')
  696. ticket_agent_role_ids = Role.joins(:permissions).where(permissions: { name: 'ticket.agent', active: true }, roles: { active: true }).pluck(:id)
  697. count = User.joins(:roles).where(roles: { id: ticket_agent_role_ids }, users: { active: true }).distinct.count + 1
  698. raise Exceptions::UnprocessableEntity, __('Agent limit exceeded, please check your account settings.') if count > Setting.get('system_agent_limit').to_i
  699. true
  700. end
  701. def validate_agent_limit_by_role(role)
  702. return true if Setting.get('system_agent_limit').blank?
  703. return true if active != true
  704. return true if role.active != true
  705. return true if !role.with_permission?('ticket.agent')
  706. ticket_agent_role_ids = Role.joins(:permissions).where(permissions: { name: 'ticket.agent', active: true }, roles: { active: true }).pluck(:id)
  707. count = User.joins(:roles).where(roles: { id: ticket_agent_role_ids }, users: { active: true }).distinct.count
  708. # if new added role is a ticket.agent role
  709. if ticket_agent_role_ids.include?(role.id)
  710. # if user already has a ticket.agent role
  711. hint = false
  712. role_ids.each do |locale_role_id|
  713. next if ticket_agent_role_ids.exclude?(locale_role_id)
  714. hint = true
  715. break
  716. end
  717. # user has not already a ticket.agent role
  718. if hint == false
  719. count += 1
  720. end
  721. end
  722. raise Exceptions::UnprocessableEntity, __('Agent limit exceeded, please check your account settings.') if count > Setting.get('system_agent_limit').to_i
  723. true
  724. end
  725. def domain_based_assignment
  726. return true if !email
  727. return true if organization_id
  728. begin
  729. domain = Mail::Address.new(email).domain
  730. return true if !domain
  731. organization = Organization.find_by(domain: domain.downcase, domain_assignment: true)
  732. return true if !organization
  733. self.organization_id = organization.id
  734. rescue
  735. return true
  736. end
  737. true
  738. end
  739. # sets locale of the user
  740. def set_locale
  741. # set the user's locale to the one of the "executing" user
  742. return true if !UserInfo.current_user_id
  743. user = UserInfo.current_user
  744. return true if !user
  745. return true if !user.preferences[:locale]
  746. preferences[:locale] = user.preferences[:locale]
  747. true
  748. end
  749. def destroy_longer_required_objects
  750. ::Avatar.remove(self.class.to_s, id)
  751. ::UserDevice.remove(id)
  752. ::StatsStore.where(stats_storable: self).destroy_all
  753. end
  754. def destroy_move_dependency_ownership
  755. result = Models.references(self.class.to_s, id)
  756. user_columns = %w[created_by_id updated_by_id out_of_office_replacement_id origin_by_id owner_id archived_by_id published_by_id internal_by_id]
  757. result.each do |class_name, references|
  758. next if class_name.blank?
  759. next if references.blank?
  760. ref_class = class_name.constantize
  761. ref_update_columns = []
  762. references.each do |column, reference_found|
  763. next if !reference_found
  764. if user_columns.include?(column)
  765. ref_update_columns << column
  766. elsif ref_class.exists?(column => id)
  767. raise "Failed deleting references! Check logic for #{class_name}->#{column}."
  768. end
  769. end
  770. next if ref_update_columns.blank?
  771. where_sql = ref_update_columns.map { |column| "#{column} = #{id}" }.join(' OR ')
  772. ref_class.where(where_sql).find_in_batches(batch_size: 1000) do |batch_list|
  773. batch_list.each do |record|
  774. ref_update_columns.each do |column|
  775. next if record[column] != id
  776. record[column] = 1
  777. end
  778. record.save!(validate: false)
  779. rescue => e
  780. Rails.logger.error e
  781. end
  782. end
  783. end
  784. true
  785. end
  786. def ensure_password
  787. return if !password_changed?
  788. self.password = ensured_password
  789. end
  790. def ensured_password
  791. # ensure unset password for blank values of new users
  792. return nil if new_record? && password.blank?
  793. # don't permit empty password update for existing users
  794. return password_was if password.blank?
  795. # don't re-hash passwords
  796. return password if PasswordHash.crypted?(password)
  797. if !PasswordPolicy::MaxLength.valid? password
  798. errors.add :password, __('is too long')
  799. return nil
  800. end
  801. # hash the plaintext password
  802. PasswordHash.crypt(password)
  803. end
  804. # reset login_failed if password is changed
  805. def reset_login_failed_after_password_change
  806. return true if !will_save_change_to_attribute?('password')
  807. self.login_failed = 0
  808. true
  809. end
  810. # When adding/removing a phone/mobile number from the User table,
  811. # update caller ID table
  812. # to adopt/orphan matching Cti::Logs accordingly
  813. # (see https://github.com/zammad/zammad/issues/2057)
  814. def update_caller_id
  815. # skip if "phone/mobile" does not change, or changes like [nil, ""]
  816. return if persisted? && previous_changes.slice(:phone, :mobile).values.flatten.none?(&:present?)
  817. return if destroyed? && phone.blank? && mobile.blank?
  818. Cti::CallerId.build(self)
  819. end
  820. end