user.rb 30 KB

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