Class: PostCreator

Inherits:
Object
  • Object
show all
Includes:
HasErrors
Defined in:
lib/post_creator.rb

Overview

Responsible for creating posts and topics

Instance Attribute Summary collapse

Attributes included from HasErrors

#conflict, #errors, #forbidden, #not_found

Class Method Summary collapse

Instance Method Summary collapse

Methods included from HasErrors

#add_error, #add_errors_from, #rollback_from_errors!, #rollback_with!, #validate_child

Constructor Details

#initialize(user, opts) ⇒ PostCreator

Acceptable options:

raw                     - raw text of post
image_sizes             - We can pass a list of the sizes of images in the post as a shortcut.
invalidate_oneboxes     - Whether to force invalidation of oneboxes in this post
acting_user             - The user performing the action might be different than the user
                          who is the post "author." For example when copying posts to a new
                          topic.
created_at              - Post creation time (optional)
auto_track              - Automatically track this topic if needed (default true)
custom_fields           - Custom fields to be added to the post, Hash (default nil)
post_type               - Whether this is a regular post or moderator post.
no_bump                 - Do not cause this post to bump the topic.
cooking_options         - Options for rendering the text
cook_method             - Method of cooking the post.
                            :regular - Pass through Markdown parser and strip bad HTML
                            :raw_html - Perform no processing
                            :raw_email - Imported from an email
via_email               - Mark this post as arriving via email
raw_email               - Full text of arriving email (to store)
action_code             - Describes a small_action post (optional)
skip_jobs               - Don't enqueue jobs when creation succeeds. This is needed if you
                          wrap `PostCreator` in a transaction, as the sidekiq jobs could
                          dequeue before the commit finishes. If you do this, be sure to
                          call `enqueue_jobs` after the transaction is committed.
hidden_reason_id        - Reason for hiding the post (optional)
skip_validations        - Do not validate any of the content in the post
draft_key               - the key of the draft we are creating (will be deleted on success)
advance_draft           - Destroy draft after creating post or topic
silent                  - Do not update topic stats and fields like last_post_user_id

When replying to a topic:
  topic_id              - topic we're replying to
  reply_to_post_number  - post number we're replying to

When creating a topic:
  title                 - New topic title
  archetype             - Topic archetype
  is_warning            - Is the topic a warning?
  category              - Category to assign to topic
  target_usernames      - comma delimited list of usernames for membership (private message)
  target_group_names    - comma delimited list of groups for membership (private message)
  meta_data             - Topic meta data hash
  created_at            - Topic creation time (optional)
  pinned_at             - Topic pinned time (optional)
  pinned_globally       - Is the topic pinned globally (optional)
  shared_draft          - Is the topic meant to be a shared draft
  topic_opts            - Options to be overwritten for topic


60
61
62
63
64
65
66
67
68
69
70
71
72
# File 'lib/post_creator.rb', line 60

def initialize(user, opts)
  # TODO: we should reload user in case it is tainted, should take in a user_id as opposed to user
  # If we don't do this we introduce a rather risky dependency
  @user = user
  @spam = false
  @opts = opts || {}

  opts[:title] = pg_clean_up(opts[:title]) if opts[:title]&.include?("\u0000")
  opts[:raw] = pg_clean_up(opts[:raw]) if opts[:raw]&.include?("\u0000")
  opts[:visible] = false if opts[:visible].nil? && opts[:hidden_reason_id].present?

  opts.delete(:reply_to_post_number) unless opts[:topic_id]
end

Instance Attribute Details

#optsObject (readonly)

Returns the value of attribute opts.



9
10
11
# File 'lib/post_creator.rb', line 9

def opts
  @opts
end

#postObject (readonly)

Returns the value of attribute post.



9
10
11
# File 'lib/post_creator.rb', line 9

def post
  @post
end

Class Method Details

.before_create_tasks(post) ⇒ Object



274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
# File 'lib/post_creator.rb', line 274

def self.before_create_tasks(post)
  set_reply_info(post)

  post.word_count = post.raw.scan(/[[:word:]]+/).size

  whisper = post.post_type == Post.types[:whisper]
  increase_posts_count =
    !post.topic&.private_message? || post.post_type != Post.types[:small_action]
  post.post_number ||=
    Topic.next_post_number(
      post.topic_id,
      reply: post.reply_to_post_number.present?,
      whisper: whisper,
      post: increase_posts_count,
    )

  cooking_options = post.cooking_options || {}
  cooking_options[:topic_id] = post.topic_id

  post.cooked ||= post.cook(post.raw, cooking_options.symbolize_keys)
  post.sort_order = post.post_number
  post.last_version_at ||= Time.now
end

.create(user, opts) ⇒ Object



266
267
268
# File 'lib/post_creator.rb', line 266

def self.create(user, opts)
  PostCreator.new(user, opts).create
end

.create!(user, opts) ⇒ Object



270
271
272
# File 'lib/post_creator.rb', line 270

def self.create!(user, opts)
  PostCreator.new(user, opts).create!
end

.set_reply_info(post) ⇒ Object



298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
# File 'lib/post_creator.rb', line 298

def self.set_reply_info(post)
  return unless post.reply_to_post_number.present?

  # Before the locking here was added, replying to a post and liking a post
  # at roughly the same time could cause a deadlock.
  #
  # Liking a post grabs an update lock on the post and then on the topic (to
  # update like counts).
  #
  # Here, we lock the replied to post before getting the topic lock so that
  # we can update the replied to post later without causing a deadlock.

  reply_info =
    Post
      .where(topic_id: post.topic_id, post_number: post.reply_to_post_number)
      .select(:user_id, :post_type)
      .lock
      .first

  if reply_info.present?
    post.reply_to_user_id ||= reply_info.user_id
    whisper_type = Post.types[:whisper]
    post.post_type = whisper_type if reply_info.post_type == whisper_type
  end
end

.track_post_statsObject



258
259
260
# File 'lib/post_creator.rb', line 258

def self.track_post_stats
  Rails.env != "test" || @track_post_stats
end

.track_post_stats=(val) ⇒ Object



262
263
264
# File 'lib/post_creator.rb', line 262

def self.track_post_stats=(val)
  @track_post_stats = val
end

Instance Method Details

#createObject



182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
# File 'lib/post_creator.rb', line 182

def create
  if valid?
    transaction do
      build_post_stats
      create_topic
      create_post_notice
      save_post
      UserActionManager.post_created(@post)
      extract_links
      track_topic
      update_topic_stats
      update_topic_auto_close
      update_user_counts
      create_embedded_topic
      @post.link_post_uploads
      @post.update_uploads_secure_status(source: "post creator")
      delete_owned_bookmarks
      ensure_in_allowed_users if guardian.is_staff?
      unarchive_message if !@opts[:import_mode]
      DraftSequence.next!(@user, draft_key) if !@opts[:import_mode] && @opts[:advance_draft]
      @post.save_reply_relationships
    end
  end

  if @post && errors.blank? && !@opts[:import_mode]
    store_unique_post_key
    # update counters etc.
    @post.topic.reload

    publish

    track_latest_on_category
    enqueue_jobs unless @opts[:skip_jobs]
    BadgeGranter.queue_badge_grant(Badge::Trigger::PostRevision, post: @post)

    trigger_after_events unless opts[:skip_events]

    auto_close
  end

  if !opts[:import_mode]
    handle_spam if (@spam || @post)

    ReviewablePost.queue_for_review_if_possible(@post, @user) if !@spam && @post && errors.blank?
  end

  @post
end

#create!Object



231
232
233
234
235
236
237
238
239
# File 'lib/post_creator.rb', line 231

def create!
  create

  if !self.errors.full_messages.empty?
    raise ActiveRecord::RecordNotSaved.new(self.errors.full_messages.to_sentence)
  end

  @post
end

#enqueue_jobsObject



241
242
243
244
245
246
247
248
249
250
251
# File 'lib/post_creator.rb', line 241

def enqueue_jobs
  return unless @post && !@post.errors.present?

  PostJobsEnqueuer.new(
    @post,
    @topic,
    new_topic?,
    import_mode: @opts[:import_mode],
    post_alert_options: @opts[:post_alert_options],
  ).enqueue_jobs
end

#guardianObject



86
87
88
# File 'lib/post_creator.rb', line 86

def guardian
  @guardian ||= @opts[:guardian] || Guardian.new(@user)
end

#pg_clean_up(str) ⇒ Object



74
75
76
# File 'lib/post_creator.rb', line 74

def pg_clean_up(str)
  str.gsub("\u0000", "")
end

#skip_validations?Boolean

Returns:

  • (Boolean)


82
83
84
# File 'lib/post_creator.rb', line 82

def skip_validations?
  @opts[:skip_validations]
end

#spam?Boolean

Returns:

  • (Boolean)


78
79
80
# File 'lib/post_creator.rb', line 78

def spam?
  @spam
end

#trigger_after_eventsObject



253
254
255
256
# File 'lib/post_creator.rb', line 253

def trigger_after_events
  DiscourseEvent.trigger(:topic_created, @post.topic, @opts, @user) unless @opts[:topic_id]
  DiscourseEvent.trigger(:post_created, @post, @opts, @user)
end

#valid?Boolean

Returns:

  • (Boolean)


90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
# File 'lib/post_creator.rb', line 90

def valid?
  @topic = nil
  @post = nil

  if @user.suspended? && !skip_validations?
    errors.add(:base, I18n.t(:user_is_suspended))
    return false
  end

  if @opts[:target_usernames].present? && !skip_validations? && !@user.staff?
    names = @opts[:target_usernames].split(",").flatten.map(&:downcase)

    # Make sure max_allowed_message_recipients setting is respected
    max_allowed_message_recipients = SiteSetting.max_allowed_message_recipients

    if names.length > max_allowed_message_recipients
      errors.add(
        :base,
        I18n.t(:max_pm_recipients, recipients_limit: max_allowed_message_recipients),
      )

      return false
    end

    # Make sure none of the users have muted or ignored the creator or prevented
    # PMs from being sent to them
    target_users = User.where(username_lower: names.map(&:downcase)).pluck(:id, :username).to_h
    UserCommScreener
      .new(acting_user: @user, target_user_ids: target_users.keys)
      .preventing_actor_communication
      .each do |user_id|
        errors.add(:base, I18n.t(:not_accepting_pms, username: target_users[user_id]))
      end

    return false if errors[:base].present?
  end

  if new_topic?
    topic_creator = TopicCreator.new(@user, guardian, @opts)
    return false unless skip_validations? || validate_child(topic_creator)
  else
    @topic = Topic.find_by(id: @opts[:topic_id])

    if @topic.present? && @opts[:archetype] == Archetype.private_message
      errors.add(:base, I18n.t(:create_pm_on_existing_topic))
      return false
    end

    if guardian.affected_by_slow_mode?(@topic)
      tu = TopicUser.find_by(user: @user, topic: @topic)

      if tu&.last_posted_at
        threshold = tu.last_posted_at + @topic.slow_mode_seconds.seconds

        if DateTime.now < threshold
          errors.add(:base, I18n.t(:slow_mode_enabled))
          return false
        end
      end
    end

    if @topic.blank? || !(@opts[:skip_guardian] || guardian.can_create?(Post, @topic))
      errors.add(:base, I18n.t(:topic_not_found))
      return false
    end
  end

  setup_post

  return true if skip_validations?

  if @post.has_host_spam?
    @spam = true
    errors.add(:base, I18n.t(:spamming_host))
    return false
  end

  DiscourseEvent.trigger :before_create_post, @post, @opts
  DiscourseEvent.trigger :validate_post, @post

  post_validator =
    PostValidator.new(
      skip_topic: true,
      private_message: @opts[:archetype] == Archetype.private_message,
    )
  post_validator.validate(@post)

  valid = @post.errors.blank?
  add_errors_from(@post) unless valid
  valid
end