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, #forbidden, #not_found

Class Method Summary collapse

Instance Method Summary collapse

Methods included from HasErrors

#add_error, #add_errors_from, #errors, #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)
  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
  embed_url             - Creates a TopicEmbed for the topic
  embed_content_sha1    - Sets the content_sha1 of the TopicEmbed


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

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[:embed_url].present? && SiteSetting.embed_unlisted?)
  )

  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



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

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



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

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

.create!(user, opts) ⇒ Object



273
274
275
# File 'lib/post_creator.rb', line 273

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

.set_reply_info(post) ⇒ Object



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

def self.set_reply_info(post)
  return if post.reply_to_post_number.blank?

  # 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



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

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

.track_post_stats=(val) ⇒ Object



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

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

Instance Method Details

#createObject



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
230
231
232
# File 'lib/post_creator.rb', line 186

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
      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] && !opts[:reviewed_queued_post]
    handle_spam if (@spam || @post)

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

  @post
end

#create!Object



234
235
236
237
238
239
240
241
242
# File 'lib/post_creator.rb', line 234

def create!
  create

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

  @post
end

#enqueue_jobsObject



244
245
246
247
248
249
250
251
252
253
254
# File 'lib/post_creator.rb', line 244

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



90
91
92
# File 'lib/post_creator.rb', line 90

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

#pg_clean_up(str) ⇒ Object



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

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

#skip_validations?Boolean

Returns:

  • (Boolean)


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

def skip_validations?
  @opts[:skip_validations]
end

#spam?Boolean

Returns:

  • (Boolean)


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

def spam?
  @spam
end

#trigger_after_eventsObject



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

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)


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
181
182
183
184
# File 'lib/post_creator.rb', line 94

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