user.rb 30 KB

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