# frozen_string_literal: true # UserHistory stores information about actions that users have taken, # like deleting users, changing site settings, dismissing notifications, etc. # Use other classes, like StaffActionLogger, to log records to this table. class UserHistory < ActiveRecord::Base belongs_to :acting_user, class_name: "User" belongs_to :target_user, class_name: "User" belongs_to :post belongs_to :topic belongs_to :category # Each value in the context should be shorter than this MAX_CONTEXT_LENGTH = 50_000 # We often store multiple values in details, particularly during post edits # Let's allow space for 2 values + a little extra for padding MAX_DETAILS_LENGTH = 110_000 MAX_JSON_LENGTH = 300_000 validates :details, length: { maximum: MAX_DETAILS_LENGTH } validates :context, length: { maximum: MAX_CONTEXT_LENGTH } validates :subject, length: { maximum: MAX_CONTEXT_LENGTH } validates :ip_address, length: { maximum: MAX_CONTEXT_LENGTH } validates :email, length: { maximum: MAX_CONTEXT_LENGTH } validates :previous_value, length: { maximum: MAX_JSON_LENGTH } validates :new_value, length: { maximum: MAX_JSON_LENGTH } validates_presence_of :action scope :only_staff_actions, -> { where("action IN (?)", UserHistory.staff_action_ids) } before_save :set_admin_only def self.actions @actions ||= Enum.new( delete_user: 1, change_trust_level: 2, change_site_setting: 3, change_theme: 4, delete_theme: 5, checked_for_custom_avatar: 6, # not used anymore notified_about_avatar: 7, notified_about_sequential_replies: 8, notified_about_dominating_topic: 9, suspend_user: 10, unsuspend_user: 11, facebook_no_email: 12, # not used anymore grant_badge: 13, revoke_badge: 14, auto_trust_level_change: 15, check_email: 16, delete_post: 17, delete_topic: 18, impersonate: 19, roll_up: 20, change_username: 21, custom: 22, custom_staff: 23, anonymize_user: 24, reviewed_post: 25, change_category_settings: 26, delete_category: 27, create_category: 28, change_site_text: 29, silence_user: 30, unsilence_user: 31, grant_admin: 32, revoke_admin: 33, grant_moderation: 34, revoke_moderation: 35, backup_create: 36, rate_limited_like: 37, # not used anymore revoke_email: 38, deactivate_user: 39, wizard_step: 40, lock_trust_level: 41, unlock_trust_level: 42, activate_user: 43, change_readonly_mode: 44, backup_download: 45, backup_destroy: 46, notified_about_get_a_room: 47, change_name: 48, post_locked: 49, post_unlocked: 50, check_personal_message: 51, disabled_second_factor: 52, post_edit: 53, topic_published: 54, recover_topic: 55, post_approved: 56, create_badge: 57, change_badge: 58, delete_badge: 59, removed_silence_user: 60, removed_suspend_user: 61, removed_unsilence_user: 62, removed_unsuspend_user: 63, post_rejected: 64, merge_user: 65, entity_export: 66, change_password: 67, topic_timestamps_changed: 68, approve_user: 69, web_hook_create: 70, web_hook_update: 71, web_hook_destroy: 72, embeddable_host_create: 73, embeddable_host_update: 74, embeddable_host_destroy: 75, web_hook_deactivate: 76, change_theme_setting: 77, disable_theme_component: 78, enable_theme_component: 79, api_key_create: 80, api_key_update: 81, api_key_destroy: 82, revoke_title: 83, change_title: 84, override_upload_secure_status: 85, page_published: 86, page_unpublished: 87, add_email: 88, update_email: 89, destroy_email: 90, topic_closed: 91, topic_opened: 92, topic_archived: 93, topic_unarchived: 94, post_staff_note_create: 95, post_staff_note_destroy: 96, watched_word_create: 97, watched_word_destroy: 98, delete_group: 99, permanently_delete_post_revisions: 100, create_public_sidebar_section: 101, update_public_sidebar_section: 102, destroy_public_sidebar_section: 103, reset_bounce_score: 104, create_watched_word_group: 105, update_watched_word_group: 106, delete_watched_word_group: 107, redirected_to_required_fields: 108, filled_in_required_fields: 109, topic_slow_mode_set: 110, topic_slow_mode_removed: 111, ) end # Staff actions is a subset of all actions, used to audit actions taken by staff users. def self.staff_actions @staff_actions ||= %i[ delete_user change_trust_level change_site_setting change_theme delete_theme change_site_text suspend_user unsuspend_user removed_suspend_user removed_unsuspend_user grant_badge revoke_badge check_email delete_post delete_topic impersonate roll_up change_username custom_staff anonymize_user reviewed_post change_category_settings delete_category create_category silence_user unsilence_user removed_silence_user removed_unsilence_user grant_admin revoke_admin grant_moderation revoke_moderation backup_create revoke_email deactivate_user lock_trust_level unlock_trust_level activate_user change_readonly_mode backup_download backup_destroy post_locked post_unlocked check_personal_message disabled_second_factor post_edit topic_published recover_topic post_approved create_badge change_badge delete_badge post_rejected merge_user entity_export change_name topic_timestamps_changed approve_user web_hook_create web_hook_update web_hook_destroy web_hook_deactivate embeddable_host_create embeddable_host_update embeddable_host_destroy change_theme_setting disable_theme_component enable_theme_component revoke_title change_title api_key_create api_key_update api_key_destroy override_upload_secure_status page_published page_unpublished add_email update_email destroy_email topic_closed topic_opened topic_archived topic_unarchived post_staff_note_create post_staff_note_destroy watched_word_create watched_word_destroy delete_group permanently_delete_post_revisions create_public_sidebar_section update_public_sidebar_section destroy_public_sidebar_section reset_bounce_score update_directory_columns deleted_unused_tags renamed_tag deleted_tag chat_channel_status_change chat_auto_remove_membership create_watched_word_group update_watched_word_group delete_watched_word_group topic_slow_mode_set topic_slow_mode_removed ] end def self.staff_action_ids @staff_action_ids ||= staff_actions.map { |a| actions[a] } end def self.admin_only_action_ids @admin_only_action_ids ||= [actions[:change_site_setting]] end def self.with_filters(filters) query = self query = query.where(action: filters[:action_id]) if filters[:action_id].present? query = query.where(custom_type: filters[:custom_type]) if filters[:custom_type].present? %i[acting_user target_user].each do |key| if filters[key] && (obj_id = User.where(username_lower: filters[key].downcase).pluck(:id)) query = query.where("#{key}_id = ?", obj_id) end end query = query.where("subject = ?", filters[:subject]) if filters[:subject] query end def self.for(user, action_type) self.where(target_user_id: user.id, action: UserHistory.actions[action_type]) end def self.exists_for_user?(user, action_type, opts = nil) opts = opts || {} result = self.where(target_user_id: user.id, action: UserHistory.actions[action_type]) result = result.where(topic_id: opts[:topic_id]) if opts[:topic_id] result.exists? end def self.staff_filters %i[action_id custom_type acting_user target_user subject action_name] end def self.staff_action_records(viewer, opts = nil) opts ||= {} custom_staff = opts[:action_id].to_i == actions[:custom_staff] if custom_staff opts[:custom_type] = opts[:action_name] else opts[:action_id] = self.actions[opts[:action_name].to_sym] if opts[:action_name] end query = self .with_filters(opts.slice(*staff_filters)) .only_staff_actions .order("id DESC") .includes(:acting_user, :target_user) query = query.where(admin_only: false) unless viewer && viewer.admin? query end def set_admin_only self.admin_only = UserHistory.admin_only_action_ids.include?(self.action) self end def new_value_is_json? [UserHistory.actions[:change_theme], UserHistory.actions[:delete_theme]].include?(action) end def previous_value_is_json? new_value_is_json? end end # == Schema Information # # Table name: user_histories # # id :integer not null, primary key # action :integer not null # acting_user_id :integer # target_user_id :integer # details :text # created_at :datetime not null # updated_at :datetime not null # context :string # ip_address :string # email :string # subject :text # previous_value :text # new_value :text # topic_id :integer # admin_only :boolean default(FALSE) # post_id :integer # custom_type :string # category_id :integer # # Indexes # # index_user_histories_on_acting_user_id_and_action_and_id (acting_user_id,action,id) # index_user_histories_on_action_and_id (action,id) # index_user_histories_on_category_id (category_id) # index_user_histories_on_subject_and_id (subject,id) # index_user_histories_on_target_user_id_and_id (target_user_id,id) # index_user_histories_on_topic_id_and_target_user_id_and_action (topic_id,target_user_id,action) #