2019-05-03 06:17:27 +08:00
|
|
|
# frozen_string_literal: true
|
|
|
|
|
2013-02-06 03:16:51 +08:00
|
|
|
require "archetype"
|
|
|
|
require "digest/sha1"
|
|
|
|
|
|
|
|
class Post < ActiveRecord::Base
|
|
|
|
include RateLimiter::OnCreateRecord
|
2013-05-07 12:39:01 +08:00
|
|
|
include Trashable
|
2017-08-15 23:46:57 +08:00
|
|
|
include Searchable
|
2014-04-28 16:31:51 +08:00
|
|
|
include HasCustomFields
|
2015-02-26 03:53:21 +08:00
|
|
|
include LimitedEdit
|
2013-02-06 03:16:51 +08:00
|
|
|
|
2024-05-07 02:18:53 +08:00
|
|
|
self.ignored_columns = [
|
2024-05-07 11:06:31 +08:00
|
|
|
"avg_time", # TODO: Remove when 20240212034010_drop_deprecated_columns has been promoted to pre-deploy
|
|
|
|
"image_url", # TODO: Remove when 20240212034010_drop_deprecated_columns has been promoted to pre-deploy
|
2024-05-07 02:18:53 +08:00
|
|
|
]
|
|
|
|
|
2021-03-24 23:22:16 +08:00
|
|
|
cattr_accessor :plugin_permitted_create_params, :plugin_permitted_update_params
|
2018-07-25 23:44:09 +08:00
|
|
|
self.plugin_permitted_create_params = {}
|
2021-03-24 23:22:16 +08:00
|
|
|
self.plugin_permitted_update_params = {}
|
2017-08-12 10:10:45 +08:00
|
|
|
|
2014-05-30 12:45:39 +08:00
|
|
|
# increase this number to force a system wide post rebake
|
2019-04-09 13:54:14 +08:00
|
|
|
# Recreate `index_for_rebake_old` when the number is increased
|
2017-12-15 07:28:07 +08:00
|
|
|
# Version 1, was the initial version
|
|
|
|
# Version 2 15-12-2017, introduces CommonMark and a huge number of onebox fixes
|
|
|
|
BAKED_VERSION = 2
|
2014-05-28 10:30:43 +08:00
|
|
|
|
2021-10-13 17:53:23 +08:00
|
|
|
# Time between the delete and permanent delete of a post
|
|
|
|
PERMANENT_DELETE_TIMER = 5.minutes
|
|
|
|
|
2013-02-07 23:45:24 +08:00
|
|
|
rate_limit
|
2013-10-10 07:32:03 +08:00
|
|
|
rate_limit :limit_posts_per_day
|
2013-02-19 14:57:14 +08:00
|
|
|
|
2013-02-06 03:16:51 +08:00
|
|
|
belongs_to :user
|
2016-12-02 14:03:31 +08:00
|
|
|
belongs_to :topic
|
2014-07-17 03:04:55 +08:00
|
|
|
|
2013-03-20 07:51:39 +08:00
|
|
|
belongs_to :reply_to_user, class_name: "User"
|
2013-02-06 03:16:51 +08:00
|
|
|
|
|
|
|
has_many :post_replies
|
|
|
|
has_many :replies, through: :post_replies
|
2021-03-18 12:22:41 +08:00
|
|
|
has_many :post_actions, dependent: :destroy
|
2013-06-14 01:41:45 +08:00
|
|
|
has_many :topic_links
|
2015-12-01 13:52:43 +08:00
|
|
|
has_many :group_mentions, dependent: :destroy
|
2013-02-06 03:16:51 +08:00
|
|
|
|
2022-06-09 07:24:30 +08:00
|
|
|
has_many :upload_references, as: :target, dependent: :destroy
|
|
|
|
has_many :uploads, through: :upload_references
|
2013-06-13 07:43:50 +08:00
|
|
|
|
2015-08-03 12:29:04 +08:00
|
|
|
has_one :post_stat
|
2022-01-06 06:56:05 +08:00
|
|
|
|
2022-05-23 08:07:15 +08:00
|
|
|
has_many :bookmarks, as: :bookmarkable
|
2013-05-23 03:33:33 +08:00
|
|
|
|
2016-04-21 03:29:27 +08:00
|
|
|
has_one :incoming_email
|
|
|
|
|
2013-10-15 22:21:30 +08:00
|
|
|
has_many :post_details
|
|
|
|
|
2013-12-12 10:41:34 +08:00
|
|
|
has_many :post_revisions
|
2018-05-29 07:34:12 +08:00
|
|
|
has_many :revisions, -> { order(:number) }, foreign_key: :post_id, class_name: "PostRevision"
|
2013-12-12 10:41:34 +08:00
|
|
|
|
2024-11-13 04:35:20 +08:00
|
|
|
has_many :moved_posts_as_old_post,
|
|
|
|
class_name: "MovedPost",
|
|
|
|
foreign_key: :old_post_id,
|
|
|
|
dependent: :destroy
|
|
|
|
has_many :moved_posts_as_new_post,
|
|
|
|
class_name: "MovedPost",
|
|
|
|
foreign_key: :new_post_id,
|
|
|
|
dependent: :destroy
|
|
|
|
|
2014-07-17 03:04:55 +08:00
|
|
|
has_many :user_actions, foreign_key: :target_post_id
|
|
|
|
|
FEATURE: Include optimized thumbnails for topics (#9215)
This introduces new APIs for obtaining optimized thumbnails for topics. There are a few building blocks required for this:
- Introduces new `image_upload_id` columns on the `posts` and `topics` table. This replaces the old `image_url` column, which means that thumbnails are now restricted to uploads. Hotlinked thumbnails are no longer possible. In normal use (with pull_hotlinked_images enabled), this has no noticeable impact
- A migration attempts to match existing urls to upload records. If a match cannot be found then the posts will be queued for rebake
- Optimized thumbnails are generated during post_process_cooked. If thumbnails are missing when serializing a topic list, then a sidekiq job is queued
- Topic lists and topics now include a `thumbnails` key, which includes all the available images:
```
"thumbnails": [
{
"max_width": null,
"max_height": null,
"url": "//example.com/original-image.png",
"width": 1380,
"height": 1840
},
{
"max_width": 1024,
"max_height": 1024,
"url": "//example.com/optimized-image.png",
"width": 768,
"height": 1024
}
]
```
- Themes can request additional thumbnail sizes by using a modifier in their `about.json` file:
```
"modifiers": {
"topic_thumbnail_sizes": [
[200, 200],
[800, 800]
],
...
```
Remember that these are generated asynchronously, so your theme should include logic to fallback to other available thumbnails if your requested size has not yet been generated
- Two new raw plugin outlets are introduced, to improve the customisability of the topic list. `topic-list-before-columns` and `topic-list-before-link`
2020-05-05 16:07:50 +08:00
|
|
|
belongs_to :image_upload, class_name: "Upload"
|
|
|
|
|
2022-05-03 20:53:32 +08:00
|
|
|
has_many :post_hotlinked_media, dependent: :destroy, class_name: "PostHotlinkedMedia"
|
2023-01-19 00:40:21 +08:00
|
|
|
has_many :reviewables, as: :target, dependent: :destroy
|
2022-05-03 20:53:32 +08:00
|
|
|
|
2019-10-02 12:01:53 +08:00
|
|
|
validates_with PostValidator, unless: :skip_validation
|
2023-07-28 19:53:49 +08:00
|
|
|
validates :edit_reason, length: { maximum: 1000 }
|
2013-02-06 03:16:51 +08:00
|
|
|
|
2020-08-21 07:52:43 +08:00
|
|
|
after_commit :index_search
|
2016-12-22 10:13:14 +08:00
|
|
|
|
2013-06-21 23:36:33 +08:00
|
|
|
# We can pass several creating options to a post via attributes
|
2019-01-02 22:24:13 +08:00
|
|
|
attr_accessor :image_sizes,
|
|
|
|
:quoted_post_numbers,
|
|
|
|
:no_bump,
|
|
|
|
:invalidate_oneboxes,
|
|
|
|
:cooking_options,
|
|
|
|
:skip_unique_check,
|
|
|
|
:skip_validation
|
2023-01-09 20:20:10 +08:00
|
|
|
|
2024-10-16 10:09:07 +08:00
|
|
|
MISSING_UPLOADS = "missing uploads"
|
|
|
|
MISSING_UPLOADS_IGNORED = "missing uploads ignored"
|
|
|
|
NOTICE = "notice"
|
2017-11-16 22:45:07 +08:00
|
|
|
|
2024-10-16 10:09:07 +08:00
|
|
|
SHORT_POST_CHARS = 1200
|
2013-02-06 03:16:51 +08:00
|
|
|
|
2019-04-10 20:39:35 +08:00
|
|
|
register_custom_field_type(MISSING_UPLOADS, :json)
|
2019-05-09 07:41:15 +08:00
|
|
|
register_custom_field_type(MISSING_UPLOADS_IGNORED, :boolean)
|
2019-04-10 20:39:35 +08:00
|
|
|
|
2020-11-11 20:49:53 +08:00
|
|
|
register_custom_field_type(NOTICE, :json)
|
|
|
|
|
2021-10-26 15:16:38 +08:00
|
|
|
scope :private_posts_for_user,
|
2023-11-29 13:38:07 +08:00
|
|
|
->(user) do
|
2021-10-26 15:16:38 +08:00
|
|
|
where(
|
2021-10-28 16:30:30 +08:00
|
|
|
"topics.id IN (#{Topic::PRIVATE_MESSAGES_SQL_USER})
|
|
|
|
OR topics.id IN (#{Topic::PRIVATE_MESSAGES_SQL_GROUP})",
|
2021-10-26 15:16:38 +08:00
|
|
|
user_id: user.id,
|
|
|
|
)
|
2023-11-29 13:38:07 +08:00
|
|
|
end
|
2017-05-12 03:58:43 +08:00
|
|
|
|
2017-09-15 02:08:16 +08:00
|
|
|
scope :by_newest, -> { order("created_at DESC, id DESC") }
|
2013-06-10 00:48:44 +08:00
|
|
|
scope :by_post_number, -> { order("post_number ASC") }
|
|
|
|
scope :with_user, -> { includes(:user) }
|
2017-09-15 02:08:16 +08:00
|
|
|
scope :created_since, ->(time_ago) { where("posts.created_at > ?", time_ago) }
|
2013-04-10 20:54:10 +08:00
|
|
|
scope :public_posts,
|
|
|
|
-> { joins(:topic).where("topics.archetype <> ?", Archetype.private_message) }
|
|
|
|
scope :private_posts,
|
|
|
|
-> { joins(:topic).where("topics.archetype = ?", Archetype.private_message) }
|
2013-04-17 04:56:18 +08:00
|
|
|
scope :with_topic_subtype, ->(subtype) { joins(:topic).where("topics.subtype = ?", subtype) }
|
2014-06-27 01:48:07 +08:00
|
|
|
scope :visible, -> { joins(:topic).where("topics.visible = true").where(hidden: false) }
|
2017-09-15 02:08:16 +08:00
|
|
|
scope :secured,
|
|
|
|
->(guardian) { where("posts.post_type IN (?)", Topic.visible_post_types(guardian&.user)) }
|
2023-01-09 20:20:10 +08:00
|
|
|
|
2016-05-21 21:17:54 +08:00
|
|
|
scope :for_mailing_list,
|
2023-11-29 13:38:07 +08:00
|
|
|
->(user, since) do
|
2017-01-14 02:46:33 +08:00
|
|
|
q =
|
|
|
|
created_since(since).joins(
|
2019-04-06 07:55:24 +08:00
|
|
|
"INNER JOIN (#{Topic.for_digest(user, Time.at(0)).select(:id).to_sql}) AS digest_topics ON digest_topics.id = posts.topic_id",
|
|
|
|
) # we want all topics with new content, regardless when they were created
|
|
|
|
.order("posts.created_at ASC")
|
2023-01-09 20:20:10 +08:00
|
|
|
|
2017-01-14 02:46:33 +08:00
|
|
|
q = q.where.not(post_type: Post.types[:whisper]) unless user.staff?
|
2019-04-06 07:55:24 +08:00
|
|
|
q
|
2023-11-29 13:38:07 +08:00
|
|
|
end
|
2023-01-09 20:20:10 +08:00
|
|
|
|
2017-10-04 08:47:53 +08:00
|
|
|
scope :raw_match,
|
2023-11-29 13:38:07 +08:00
|
|
|
->(pattern, type = "string") do
|
2017-10-04 08:47:53 +08:00
|
|
|
type = type&.downcase
|
2023-01-09 20:20:10 +08:00
|
|
|
|
2017-10-04 08:47:53 +08:00
|
|
|
case type
|
|
|
|
when "string"
|
|
|
|
where("raw ILIKE ?", "%#{pattern}%")
|
|
|
|
when "regex"
|
2018-08-23 20:49:00 +08:00
|
|
|
where("raw ~* ?", "(?n)#{pattern}")
|
2017-10-04 08:47:53 +08:00
|
|
|
end
|
2023-11-29 13:38:07 +08:00
|
|
|
end
|
2014-03-07 17:44:04 +08:00
|
|
|
|
2019-04-10 16:22:35 +08:00
|
|
|
scope :have_uploads,
|
2023-11-29 13:38:07 +08:00
|
|
|
-> do
|
2019-09-25 01:47:59 +08:00
|
|
|
where(
|
|
|
|
"
|
|
|
|
(
|
|
|
|
posts.cooked LIKE '%<a %' OR
|
|
|
|
posts.cooked LIKE '%<img %' OR
|
|
|
|
posts.cooked LIKE '%<video %'
|
|
|
|
) AND (
|
|
|
|
posts.cooked LIKE ? OR
|
|
|
|
posts.cooked LIKE '%/original/%' OR
|
|
|
|
posts.cooked LIKE '%/optimized/%' OR
|
|
|
|
posts.cooked LIKE '%data-orig-src=%' OR
|
|
|
|
posts.cooked LIKE '%/uploads/short-url/%'
|
|
|
|
)",
|
|
|
|
"%/uploads/#{RailsMultisite::ConnectionManagement.current_db}/%",
|
|
|
|
)
|
2023-11-29 13:38:07 +08:00
|
|
|
end
|
2019-04-10 16:22:35 +08:00
|
|
|
|
2014-01-15 00:15:35 +08:00
|
|
|
delegate :username, to: :user
|
2014-03-07 17:44:04 +08:00
|
|
|
|
2013-03-19 02:59:34 +08:00
|
|
|
def self.hidden_reasons
|
2016-01-08 18:53:52 +08:00
|
|
|
@hidden_reasons ||=
|
|
|
|
Enum.new(
|
|
|
|
flag_threshold_reached: 1,
|
|
|
|
flag_threshold_reached_again: 2,
|
|
|
|
new_user_spam_threshold_reached: 3,
|
2018-07-05 17:07:46 +08:00
|
|
|
flagged_by_tl3_user: 4,
|
2018-10-10 23:50:00 +08:00
|
|
|
email_spam_header_found: 5,
|
2019-11-26 22:55:22 +08:00
|
|
|
flagged_by_tl4_user: 6,
|
2020-04-14 03:17:02 +08:00
|
|
|
email_authentication_result_header: 7,
|
|
|
|
imported_as_unlisted: 8,
|
|
|
|
)
|
2013-03-19 02:59:34 +08:00
|
|
|
end
|
|
|
|
|
2013-03-19 04:03:46 +08:00
|
|
|
def self.types
|
2016-01-08 18:53:52 +08:00
|
|
|
@types ||= Enum.new(regular: 1, moderator_action: 2, small_action: 3, whisper: 4)
|
2013-03-19 04:03:46 +08:00
|
|
|
end
|
|
|
|
|
2014-01-01 03:37:43 +08:00
|
|
|
def self.cook_methods
|
2016-01-08 18:53:52 +08:00
|
|
|
@cook_methods ||= Enum.new(regular: 1, raw_html: 2, email: 3)
|
2014-01-01 03:37:43 +08:00
|
|
|
end
|
|
|
|
|
2019-04-19 22:53:58 +08:00
|
|
|
def self.notices
|
|
|
|
@notices ||= Enum.new(custom: "custom", new_user: "new_user", returning_user: "returning_user")
|
|
|
|
end
|
|
|
|
|
2013-10-15 22:21:30 +08:00
|
|
|
def self.find_by_detail(key, value)
|
2014-05-06 21:41:59 +08:00
|
|
|
includes(:post_details).find_by(post_details: { key: key, value: value })
|
2013-10-15 22:21:30 +08:00
|
|
|
end
|
|
|
|
|
2021-01-21 09:37:47 +08:00
|
|
|
def self.find_by_number(topic_id, post_number)
|
|
|
|
find_by(topic_id: topic_id, post_number: post_number)
|
|
|
|
end
|
|
|
|
|
2016-01-12 00:47:17 +08:00
|
|
|
def whisper?
|
|
|
|
post_type == Post.types[:whisper]
|
|
|
|
end
|
|
|
|
|
2013-10-15 22:21:30 +08:00
|
|
|
def add_detail(key, value, extra = nil)
|
|
|
|
post_details.build(key: key, value: value, extra: extra)
|
|
|
|
end
|
|
|
|
|
2013-10-10 07:32:03 +08:00
|
|
|
def limit_posts_per_day
|
2016-06-21 04:38:15 +08:00
|
|
|
if user && user.new_user_posting_on_first_day? && post_number && post_number > 1
|
2015-02-11 14:45:46 +08:00
|
|
|
RateLimiter.new(
|
|
|
|
user,
|
|
|
|
"first-day-replies-per-day",
|
|
|
|
SiteSetting.max_replies_in_first_day,
|
|
|
|
1.day.to_i,
|
|
|
|
)
|
2013-10-10 07:32:03 +08:00
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2019-09-09 09:29:15 +08:00
|
|
|
def readers_count
|
|
|
|
read_count = reads - 1 # Excludes poster
|
|
|
|
read_count < 0 ? 0 : read_count
|
|
|
|
end
|
|
|
|
|
2018-12-06 04:27:49 +08:00
|
|
|
def publish_change_to_clients!(type, opts = {})
|
|
|
|
# special failsafe for posts missing topics consistency checks should fix,
|
|
|
|
# but message is safe to skip
|
2015-09-11 04:01:23 +08:00
|
|
|
return unless topic
|
|
|
|
|
2022-06-28 05:21:05 +08:00
|
|
|
skip_topic_stats = opts.delete(:skip_topic_stats)
|
|
|
|
|
2018-12-06 04:27:49 +08:00
|
|
|
message = {
|
2015-09-22 06:50:52 +08:00
|
|
|
id: id,
|
|
|
|
post_number: post_number,
|
|
|
|
updated_at: Time.now,
|
2015-10-12 09:45:04 +08:00
|
|
|
user_id: user_id,
|
|
|
|
last_editor_id: last_editor_id,
|
2017-01-20 14:37:22 +08:00
|
|
|
type: type,
|
|
|
|
version: version,
|
2018-12-06 04:27:49 +08:00
|
|
|
}.merge(opts)
|
|
|
|
|
|
|
|
publish_message!("/topic/#{topic_id}", message)
|
2022-06-28 05:21:05 +08:00
|
|
|
Topic.publish_stats_to_clients!(topic.id, type) unless skip_topic_stats
|
2018-12-06 04:27:49 +08:00
|
|
|
end
|
|
|
|
|
|
|
|
def publish_message!(channel, message, opts = {})
|
|
|
|
return unless topic
|
2015-09-22 06:50:52 +08:00
|
|
|
|
2015-09-25 08:15:58 +08:00
|
|
|
if Topic.visible_post_types.include?(post_type)
|
2022-06-28 05:21:05 +08:00
|
|
|
opts.merge!(topic.secure_audience_publish_messages)
|
2015-09-25 08:15:58 +08:00
|
|
|
else
|
2018-12-06 08:20:36 +08:00
|
|
|
opts[:user_ids] = User.human_users.where("admin OR moderator OR id = ?", user_id).pluck(:id)
|
2015-09-11 04:01:23 +08:00
|
|
|
end
|
2018-12-06 04:27:49 +08:00
|
|
|
|
2020-09-15 14:15:42 +08:00
|
|
|
MessageBus.publish(channel, message, opts) if opts[:user_ids] != [] && opts[:group_ids] != []
|
2014-08-29 11:34:32 +08:00
|
|
|
end
|
|
|
|
|
2013-07-10 03:20:18 +08:00
|
|
|
def trash!(trashed_by = nil)
|
2013-06-14 01:41:45 +08:00
|
|
|
self.topic_links.each(&:destroy)
|
2020-11-11 20:49:53 +08:00
|
|
|
self.save_custom_fields if self.custom_fields.delete(Post::NOTICE)
|
2013-07-10 03:20:18 +08:00
|
|
|
super(trashed_by)
|
2013-06-14 01:41:45 +08:00
|
|
|
end
|
|
|
|
|
2013-05-07 12:39:01 +08:00
|
|
|
def recover!
|
|
|
|
super
|
2018-10-02 23:25:08 +08:00
|
|
|
recover_public_post_actions
|
2013-06-14 01:41:45 +08:00
|
|
|
TopicLink.extract_from(self)
|
2014-07-15 15:47:24 +08:00
|
|
|
QuotedPost.extract_from(self)
|
2013-10-24 07:05:51 +08:00
|
|
|
topic.category.update_latest if topic && topic.category_id && topic.category
|
2013-05-07 12:39:01 +08:00
|
|
|
end
|
|
|
|
|
2013-03-22 18:18:48 +08:00
|
|
|
# The key we use in redis to ensure unique posts
|
2013-02-06 03:16:51 +08:00
|
|
|
def unique_post_key
|
2022-06-29 13:35:07 +08:00
|
|
|
"unique#{topic&.private_message? ? "-pm" : ""}-post-#{user_id}:#{raw_hash}"
|
2013-02-06 03:16:51 +08:00
|
|
|
end
|
|
|
|
|
2013-09-10 04:17:31 +08:00
|
|
|
def store_unique_post_key
|
|
|
|
if SiteSetting.unique_posts_mins > 0
|
2019-12-03 17:05:53 +08:00
|
|
|
Discourse.redis.setex(unique_post_key, SiteSetting.unique_posts_mins.minutes.to_i, id)
|
2013-09-10 04:17:31 +08:00
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
def matches_recent_post?
|
2019-12-03 17:05:53 +08:00
|
|
|
post_id = Discourse.redis.get(unique_post_key)
|
2019-03-14 08:15:09 +08:00
|
|
|
post_id != (nil) && post_id.to_i != (id)
|
2013-09-10 04:17:31 +08:00
|
|
|
end
|
|
|
|
|
2013-02-06 03:16:51 +08:00
|
|
|
def raw_hash
|
2013-03-01 02:54:12 +08:00
|
|
|
return if raw.blank?
|
2014-06-16 10:14:06 +08:00
|
|
|
Digest::SHA1.hexdigest(raw)
|
2013-02-06 03:16:51 +08:00
|
|
|
end
|
|
|
|
|
2020-07-27 08:23:54 +08:00
|
|
|
def self.allowed_image_classes
|
|
|
|
@allowed_image_classes ||= %w[avatar favicon thumbnail emoji ytp-thumbnail-image]
|
2013-02-12 15:43:48 +08:00
|
|
|
end
|
|
|
|
|
2013-05-31 02:34:44 +08:00
|
|
|
def post_analyzer
|
2013-07-23 04:24:47 +08:00
|
|
|
@post_analyzers ||= {}
|
|
|
|
@post_analyzers[raw_hash] ||= PostAnalyzer.new(raw, topic_id)
|
2013-05-31 02:34:44 +08:00
|
|
|
end
|
2013-02-12 15:43:48 +08:00
|
|
|
|
2018-02-09 07:26:56 +08:00
|
|
|
%w[
|
|
|
|
raw_mentions
|
|
|
|
linked_hosts
|
2020-08-08 00:08:59 +08:00
|
|
|
embedded_media_count
|
2018-02-09 07:26:56 +08:00
|
|
|
attachment_count
|
|
|
|
link_count
|
|
|
|
raw_links
|
|
|
|
has_oneboxes?
|
2019-05-07 09:27:05 +08:00
|
|
|
].each { |attr| define_method(attr) { post_analyzer.public_send(attr) } }
|
2013-05-31 02:34:44 +08:00
|
|
|
|
2016-08-13 03:28:54 +08:00
|
|
|
def add_nofollow?
|
2018-09-17 10:02:20 +08:00
|
|
|
return false if user&.staff?
|
2016-08-16 00:57:58 +08:00
|
|
|
user.blank? || SiteSetting.tl3_links_no_follow? || !user.has_trust_level?(TrustLevel[3])
|
2016-08-13 03:28:54 +08:00
|
|
|
end
|
|
|
|
|
|
|
|
def omit_nofollow?
|
2016-08-16 00:57:58 +08:00
|
|
|
!add_nofollow?
|
2016-08-13 03:28:54 +08:00
|
|
|
end
|
|
|
|
|
2017-10-18 02:37:51 +08:00
|
|
|
def cook(raw, opts = {})
|
2014-01-01 03:37:43 +08:00
|
|
|
# For some posts, for example those imported via RSS, we support raw HTML. In that
|
|
|
|
# case we can skip the rendering pipeline.
|
|
|
|
return raw if cook_method == Post.cook_methods[:raw_html]
|
|
|
|
|
2017-10-18 02:37:51 +08:00
|
|
|
options = opts.dup
|
|
|
|
options[:cook_method] = cook_method
|
|
|
|
|
FEATURE: Generic hashtag autocomplete lookup and markdown cooking (#18937)
This commit fleshes out and adds functionality for the new `#hashtag` search and
lookup system, still hidden behind the `enable_experimental_hashtag_autocomplete`
feature flag.
**Serverside**
We have two plugin API registration methods that are used to define data sources
(`register_hashtag_data_source`) and hashtag result type priorities depending on
the context (`register_hashtag_type_in_context`). Reading the comments in plugin.rb
should make it clear what these are doing. Reading the `HashtagAutocompleteService`
in full will likely help a lot as well.
Each data source is responsible for providing its own **lookup** and **search**
method that returns hashtag results based on the arguments provided. For example,
the category hashtag data source has to take into account parent categories and
how they relate, and each data source has to define their own icon to use for the
hashtag, and so on.
The `Site` serializer has two new attributes that source data from `HashtagAutocompleteService`.
There is `hashtag_icons` that is just a simple array of all the different icons that
can be used for allowlisting in our markdown pipeline, and there is `hashtag_context_configurations`
that is used to store the type priority orders for each registered context.
When sending emails, we cannot render the SVG icons for hashtags, so
we need to change the HTML hashtags to the normal `#hashtag` text.
**Markdown**
The `hashtag-autocomplete.js` file is where I have added the new `hashtag-autocomplete`
markdown rule, and like all of our rules this is used to cook the raw text on both the clientside
and on the serverside using MiniRacer. Only on the server side do we actually reach out to
the database with the `hashtagLookup` function, on the clientside we just render a plainer
version of the hashtag HTML. Only in the composer preview do we do further lookups based
on this.
This rule is the first one (that I can find) that uses the `currentUser` based on a passed
in `user_id` for guardian checks in markdown rendering code. This is the `last_editor_id`
for both the post and chat message. In some cases we need to cook without a user present,
so the `Discourse.system_user` is used in this case.
**Chat Channels**
This also contains the changes required for chat so that chat channels can be used
as a data source for hashtag searches and lookups. This data source will only be
used when `enable_experimental_hashtag_autocomplete` is `true`, so we don't have
to worry about channel results suddenly turning up.
------
**Known Rough Edges**
- Onebox excerpts will not render the icon svg/use tags, I plan to address that in a follow up PR
- Selecting a hashtag + pressing the Quote button will result in weird behaviour, I plan to address that in a follow up PR
- Mixed hashtag contexts for hashtags without a type suffix will not work correctly, e.g. #ux which is both a category and a channel slug will resolve to a category when used inside a post or within a [chat] transcript in that post. Users can get around this manually by adding the correct suffix, for example ::channel. We may get to this at some point in future
- Icons will not show for the hashtags in emails since SVG support is so terrible in email (this is not likely to be resolved, but still noting for posterity)
- Additional refinements and review fixes wil
2022-11-21 06:37:06 +08:00
|
|
|
# A rule in our Markdown pipeline may have Guardian checks that require a
|
|
|
|
# user to be present. The last editing user of the post will be more
|
|
|
|
# generally up to date than the creating user. For example, we use
|
|
|
|
# this when cooking #hashtags to determine whether we should render
|
|
|
|
# the found hashtag based on whether the user can access the category it
|
|
|
|
# is referencing.
|
|
|
|
options[:user_id] = self.last_editor_id
|
2018-09-17 10:02:20 +08:00
|
|
|
options[:omit_nofollow] = true if omit_nofollow?
|
FIX: Add post id to the anchor to prevent two identical anchors (#28070)
* FIX: Add post id to the anchor to prevent two identical anchors
We generate anchors for headings in posts. This works fine if there is
only one post in a topic with anchors. The problem comes when you have
two or more posts with the same heading. PrettyText generates anchors
based on the heading text using the raw context of each post, so it is
entirely possible to generate the same anchor for two posts in the same
topic, especially for topics with template replies
Post1:
# heading
context
Post2:
# heading
context
When both posts are on the page at the same time, the anchor will only
work for the first post, according to the [HTML specification](https://html.spec.whatwg.org/multipage/browsing-the-web.html#scroll-to-the-fragment-identifier).
> If there is an a element in the document tree whose root is document
> that has a name attribute whose value is equal to fragment, then
> return the *first* such element in tree order.
This bug is particularly serious in forums with non-Latin languages,
such as Chinese. We do not generate slugs for Chinese, which results in
the heading anchors being completely dependent on their order.
```ruby
[2] pry(main)> PrettyText.cook("# 中文")
=> "<h1><a name=\"h-1\" class=\"anchor\" href=\"#h-1\"></a>中文</h1>"
```
Therefore, the anchors in the two posts must be in exactly the same by
order, causing almost all of the anchors in the second post to be
invalid.
This commit solves this problem by adding the `post_id` to the anchor.
The new anchor generation method will add `p-{post_id}` as a prefix when
post_id is available:
```ruby
[3] pry(main)> PrettyText.cook("# 中文", post_id: 1234)
=> "<h1><a name=\"p-1234-h-1\" class=\"anchor\" href=\"#p-1234-h-1\"></a>中文</h1>"
```
This way we can ensure that each anchor name only appears once on the
same topic. Using post id also prevents the potential possibility of the
same anchor name when splitting/merging topics.
2024-07-25 13:50:30 +08:00
|
|
|
options[:post_id] = self.id
|
2017-10-18 02:37:51 +08:00
|
|
|
|
2024-04-09 11:23:11 +08:00
|
|
|
if self.should_secure_uploads?
|
2019-11-18 09:25:42 +08:00
|
|
|
each_upload_url do |url|
|
|
|
|
uri = URI.parse(url)
|
|
|
|
if FileHelper.is_supported_media?(File.basename(uri.path))
|
2020-08-28 09:28:11 +08:00
|
|
|
raw =
|
|
|
|
raw.sub(
|
|
|
|
url,
|
|
|
|
Rails.application.routes.url_for(
|
|
|
|
controller: "uploads",
|
|
|
|
action: "show_secure",
|
|
|
|
path: uri.path[1..-1],
|
|
|
|
host: Discourse.current_hostname,
|
2023-01-09 20:20:10 +08:00
|
|
|
),
|
2020-08-28 09:28:11 +08:00
|
|
|
)
|
2019-11-18 09:25:42 +08:00
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2018-09-17 10:02:20 +08:00
|
|
|
cooked = post_analyzer.cook(raw, options)
|
2014-11-24 07:34:29 +08:00
|
|
|
|
|
|
|
new_cooked = Plugin::Filter.apply(:after_post_cook, self, cooked)
|
|
|
|
|
2015-07-30 02:54:33 +08:00
|
|
|
if post_type == Post.types[:regular]
|
|
|
|
if new_cooked != cooked && new_cooked.blank?
|
2017-10-18 02:37:51 +08:00
|
|
|
Rails.logger.debug("Plugin is blanking out post: #{self.url}\nraw: #{raw}")
|
2015-07-30 02:54:33 +08:00
|
|
|
elsif new_cooked.blank?
|
2017-10-18 02:37:51 +08:00
|
|
|
Rails.logger.debug("Blank post detected post: #{self.url}\nraw: #{raw}")
|
2015-07-30 02:54:33 +08:00
|
|
|
end
|
2014-11-24 07:34:29 +08:00
|
|
|
end
|
|
|
|
|
|
|
|
new_cooked
|
2013-02-06 03:16:51 +08:00
|
|
|
end
|
|
|
|
|
2013-04-06 01:59:00 +08:00
|
|
|
# Sometimes the post is being edited by someone else, for example, a mod.
|
|
|
|
# If that's the case, they should not be bound by the original poster's
|
|
|
|
# restrictions, for example on not posting images.
|
|
|
|
def acting_user
|
|
|
|
@acting_user || user
|
|
|
|
end
|
|
|
|
|
|
|
|
def acting_user=(pu)
|
|
|
|
@acting_user = pu
|
|
|
|
end
|
|
|
|
|
2016-03-09 04:26:06 +08:00
|
|
|
def last_editor
|
|
|
|
self.last_editor_id ? (User.find_by_id(self.last_editor_id) || user) : user
|
|
|
|
end
|
|
|
|
|
2020-07-27 08:23:54 +08:00
|
|
|
def allowed_spam_hosts
|
2014-02-27 12:43:45 +08:00
|
|
|
hosts =
|
|
|
|
SiteSetting
|
2020-07-27 08:23:54 +08:00
|
|
|
.allowed_spam_host_domains
|
2014-03-30 07:50:44 +08:00
|
|
|
.split("|")
|
2014-02-27 12:43:45 +08:00
|
|
|
.map { |h| h.strip }
|
2014-03-30 07:50:44 +08:00
|
|
|
.reject { |h| !h.include?(".") }
|
2014-02-27 12:43:45 +08:00
|
|
|
|
|
|
|
hosts << GlobalSetting.hostname
|
2014-04-28 22:37:28 +08:00
|
|
|
hosts << RailsMultisite::ConnectionManagement.current_hostname
|
2014-02-27 12:43:45 +08:00
|
|
|
end
|
|
|
|
|
2013-05-11 04:58:23 +08:00
|
|
|
def total_hosts_usage
|
|
|
|
hosts = linked_hosts.clone
|
2020-07-27 08:23:54 +08:00
|
|
|
allowlisted = allowed_spam_hosts
|
2014-02-27 12:43:45 +08:00
|
|
|
|
2020-07-27 08:23:54 +08:00
|
|
|
hosts.reject! { |h| allowlisted.any? { |w| h.end_with?(w) } }
|
2014-02-27 12:43:45 +08:00
|
|
|
|
|
|
|
return hosts if hosts.length == 0
|
2013-05-11 04:58:23 +08:00
|
|
|
|
2013-05-25 03:20:58 +08:00
|
|
|
TopicLink
|
|
|
|
.where(domain: hosts.keys, user_id: acting_user.id)
|
|
|
|
.group(:domain, :post_id)
|
2016-04-26 05:03:17 +08:00
|
|
|
.count
|
|
|
|
.each_key do |tuple|
|
2013-05-25 03:20:58 +08:00
|
|
|
domain = tuple[0]
|
|
|
|
hosts[domain] = (hosts[domain] || 0) + 1
|
2013-05-11 04:58:23 +08:00
|
|
|
end
|
|
|
|
|
|
|
|
hosts
|
|
|
|
end
|
|
|
|
|
|
|
|
# Prevent new users from posting the same hosts too many times.
|
|
|
|
def has_host_spam?
|
2018-06-19 08:05:04 +08:00
|
|
|
if acting_user.present? &&
|
|
|
|
(
|
|
|
|
acting_user.staged? || acting_user.mature_staged? ||
|
|
|
|
acting_user.has_trust_level?(TrustLevel[1])
|
2023-01-09 20:20:10 +08:00
|
|
|
)
|
2018-06-19 08:05:04 +08:00
|
|
|
return false
|
2023-01-09 20:20:10 +08:00
|
|
|
end
|
2017-08-11 05:18:57 +08:00
|
|
|
return false if topic&.private_message?
|
2013-05-11 04:58:23 +08:00
|
|
|
|
2016-04-26 05:03:17 +08:00
|
|
|
total_hosts_usage.values.any? { |count| count >= SiteSetting.newuser_spam_host_threshold }
|
2013-05-11 04:58:23 +08:00
|
|
|
end
|
|
|
|
|
2013-02-06 03:16:51 +08:00
|
|
|
def archetype
|
2017-09-13 01:04:53 +08:00
|
|
|
topic&.archetype
|
2013-02-06 03:16:51 +08:00
|
|
|
end
|
2013-02-07 23:45:24 +08:00
|
|
|
|
2013-02-06 03:16:51 +08:00
|
|
|
def self.regular_order
|
2013-02-07 23:45:24 +08:00
|
|
|
order(:sort_order, :post_number)
|
2013-02-06 03:16:51 +08:00
|
|
|
end
|
|
|
|
|
|
|
|
def self.reverse_order
|
2013-02-07 23:45:24 +08:00
|
|
|
order("sort_order desc, post_number desc")
|
2013-02-06 03:16:51 +08:00
|
|
|
end
|
|
|
|
|
2018-06-21 12:00:54 +08:00
|
|
|
def self.summary(topic_id)
|
|
|
|
topic_id = topic_id.to_i
|
2015-01-30 14:19:42 +08:00
|
|
|
|
|
|
|
# percent rank has tons of ties
|
2018-06-21 12:00:54 +08:00
|
|
|
where(topic_id: topic_id).where(
|
2018-06-21 13:26:26 +08:00
|
|
|
[
|
2023-06-27 22:44:34 +08:00
|
|
|
"posts.id = ANY(
|
2018-06-21 13:26:26 +08:00
|
|
|
(
|
|
|
|
SELECT posts.id
|
|
|
|
FROM posts
|
|
|
|
WHERE posts.topic_id = #{topic_id.to_i}
|
2018-06-21 14:00:20 +08:00
|
|
|
AND posts.post_number = 1
|
2018-06-21 13:26:26 +08:00
|
|
|
) UNION
|
|
|
|
(
|
|
|
|
SELECT p1.id
|
|
|
|
FROM posts p1
|
|
|
|
WHERE p1.percent_rank <= ?
|
|
|
|
AND p1.topic_id = #{topic_id.to_i}
|
|
|
|
ORDER BY p1.percent_rank
|
|
|
|
LIMIT ?
|
|
|
|
)
|
2018-06-21 12:00:54 +08:00
|
|
|
)",
|
|
|
|
SiteSetting.summary_percent_filter.to_f / 100.0,
|
|
|
|
SiteSetting.summary_max_results,
|
|
|
|
],
|
|
|
|
)
|
2013-02-06 03:16:51 +08:00
|
|
|
end
|
|
|
|
|
2019-03-08 16:48:35 +08:00
|
|
|
def delete_post_notices
|
2020-11-11 20:49:53 +08:00
|
|
|
self.custom_fields.delete(Post::NOTICE)
|
2019-03-11 17:19:58 +08:00
|
|
|
self.save_custom_fields
|
2019-03-08 16:48:35 +08:00
|
|
|
end
|
|
|
|
|
2018-10-02 23:25:08 +08:00
|
|
|
def recover_public_post_actions
|
|
|
|
PostAction
|
|
|
|
.publics
|
|
|
|
.with_deleted
|
|
|
|
.where(post_id: self.id, id: self.custom_fields["deleted_public_actions"])
|
|
|
|
.find_each do |post_action|
|
|
|
|
post_action.recover!
|
|
|
|
post_action.save!
|
|
|
|
end
|
|
|
|
|
|
|
|
self.custom_fields.delete("deleted_public_actions")
|
|
|
|
self.save_custom_fields
|
|
|
|
end
|
|
|
|
|
2013-03-01 02:54:12 +08:00
|
|
|
def filter_quotes(parent_post = nil)
|
2013-02-06 03:16:51 +08:00
|
|
|
return cooked if parent_post.blank?
|
|
|
|
|
|
|
|
# We only filter quotes when there is exactly 1
|
|
|
|
return cooked unless (quote_count == 1)
|
|
|
|
|
2013-02-16 09:58:33 +08:00
|
|
|
parent_raw = parent_post.raw.sub(%r{\[quote.+/quote\]}m, "")
|
2013-02-06 03:16:51 +08:00
|
|
|
|
2013-03-05 08:42:44 +08:00
|
|
|
if raw[parent_raw] || (parent_raw.size < SHORT_POST_CHARS)
|
2013-02-06 03:16:51 +08:00
|
|
|
return cooked.sub(%r{\<aside.+\</aside\>}m, "")
|
|
|
|
end
|
|
|
|
|
|
|
|
cooked
|
|
|
|
end
|
|
|
|
|
|
|
|
def external_id
|
2013-02-07 23:45:24 +08:00
|
|
|
"#{topic_id}/#{post_number}"
|
2013-02-06 03:16:51 +08:00
|
|
|
end
|
|
|
|
|
2014-01-04 01:52:24 +08:00
|
|
|
def reply_to_post
|
|
|
|
return if reply_to_post_number.blank?
|
2014-05-06 21:41:59 +08:00
|
|
|
@reply_to_post ||=
|
|
|
|
Post.find_by(
|
|
|
|
"topic_id = :topic_id AND post_number = :post_number",
|
|
|
|
topic_id: topic_id,
|
|
|
|
post_number: reply_to_post_number,
|
|
|
|
)
|
2014-01-04 01:52:24 +08:00
|
|
|
end
|
|
|
|
|
2013-02-06 03:16:51 +08:00
|
|
|
def reply_notification_target
|
2013-03-01 02:54:12 +08:00
|
|
|
return if reply_to_post_number.blank?
|
2014-05-06 21:41:59 +08:00
|
|
|
Post.find_by(
|
|
|
|
"topic_id = :topic_id AND post_number = :post_number AND user_id <> :user_id",
|
|
|
|
topic_id: topic_id,
|
|
|
|
post_number: reply_to_post_number,
|
|
|
|
user_id: user_id,
|
|
|
|
).try(:user)
|
2013-02-06 03:16:51 +08:00
|
|
|
end
|
|
|
|
|
2013-04-30 11:25:55 +08:00
|
|
|
def self.excerpt(cooked, maxlength = nil, options = {})
|
2013-02-06 03:16:51 +08:00
|
|
|
maxlength ||= SiteSetting.post_excerpt_maxlength
|
2013-04-30 11:25:55 +08:00
|
|
|
PrettyText.excerpt(cooked, maxlength, options)
|
2013-02-06 03:16:51 +08:00
|
|
|
end
|
|
|
|
|
|
|
|
# Strip out most of the markup
|
2013-04-30 11:25:55 +08:00
|
|
|
def excerpt(maxlength = nil, options = {})
|
2019-05-29 23:05:52 +08:00
|
|
|
Post.excerpt(cooked, maxlength, options.merge(post: self))
|
2013-02-06 03:16:51 +08:00
|
|
|
end
|
|
|
|
|
2018-04-18 03:08:13 +08:00
|
|
|
def excerpt_for_topic
|
2021-01-11 10:43:11 +08:00
|
|
|
Post.excerpt(
|
|
|
|
cooked,
|
|
|
|
SiteSetting.topic_excerpt_maxlength,
|
|
|
|
strip_links: true,
|
|
|
|
strip_images: true,
|
|
|
|
post: self,
|
|
|
|
)
|
2018-04-18 03:08:13 +08:00
|
|
|
end
|
|
|
|
|
2013-05-26 08:18:04 +08:00
|
|
|
def is_first_post?
|
2015-04-24 01:33:29 +08:00
|
|
|
post_number.blank? ? topic.try(:highest_post_number) == 0 : post_number == 1
|
2013-05-26 08:18:04 +08:00
|
|
|
end
|
|
|
|
|
2020-07-23 21:50:00 +08:00
|
|
|
def is_category_description?
|
|
|
|
topic.present? && topic.is_category_topic? && is_first_post?
|
|
|
|
end
|
|
|
|
|
2016-08-11 01:24:01 +08:00
|
|
|
def is_reply_by_email?
|
|
|
|
via_email && post_number.present? && post_number > 1
|
|
|
|
end
|
|
|
|
|
2013-02-07 23:45:24 +08:00
|
|
|
def is_flagged?
|
2023-04-20 15:49:35 +08:00
|
|
|
flags.count != 0
|
|
|
|
end
|
|
|
|
|
FIX: serialize Flags instead of PostActionType (#28362)
### Why?
Before, all flags were static. Therefore, they were stored in class variables and serialized by SiteSerializer. Recently, we added an option for admins to add their own flags or disable existing flags. Therefore, the class variable had to be dropped because it was unsafe for a multisite environment. However, it started causing performance problems.
### Solution
When a new Flag system is used, instead of using PostActionType, we can serialize Flags and use fragment cache for performance reasons.
At the same time, we are still supporting deprecated `replace_flags` API call. When it is used, we fall back to the old solution and the admin cannot add custom flags. In a couple of months, we will be able to drop that API function and clean that code properly. However, because it may still be used, redis cache was introduced to improve performance.
To test backward compatibility you can add this code to any plugin
```ruby
replace_flags do |flag_settings|
flag_settings.add(
4,
:inappropriate,
topic_type: true,
notify_type: true,
auto_action_type: true,
)
flag_settings.add(1001, :trolling, topic_type: true, notify_type: true, auto_action_type: true)
end
```
2024-08-14 10:13:46 +08:00
|
|
|
def post_action_type_view
|
|
|
|
@post_action_type_view ||= PostActionTypeView.new
|
|
|
|
end
|
|
|
|
|
2023-04-20 15:49:35 +08:00
|
|
|
def flags
|
2017-10-18 01:31:45 +08:00
|
|
|
post_actions.where(
|
FIX: serialize Flags instead of PostActionType (#28362)
### Why?
Before, all flags were static. Therefore, they were stored in class variables and serialized by SiteSerializer. Recently, we added an option for admins to add their own flags or disable existing flags. Therefore, the class variable had to be dropped because it was unsafe for a multisite environment. However, it started causing performance problems.
### Solution
When a new Flag system is used, instead of using PostActionType, we can serialize Flags and use fragment cache for performance reasons.
At the same time, we are still supporting deprecated `replace_flags` API call. When it is used, we fall back to the old solution and the admin cannot add custom flags. In a couple of months, we will be able to drop that API function and clean that code properly. However, because it may still be used, redis cache was introduced to improve performance.
To test backward compatibility you can add this code to any plugin
```ruby
replace_flags do |flag_settings|
flag_settings.add(
4,
:inappropriate,
topic_type: true,
notify_type: true,
auto_action_type: true,
)
flag_settings.add(1001, :trolling, topic_type: true, notify_type: true, auto_action_type: true)
end
```
2024-08-14 10:13:46 +08:00
|
|
|
post_action_type_id: post_action_type_view.flag_types_without_additional_message.values,
|
2017-10-18 01:31:45 +08:00
|
|
|
deleted_at: nil,
|
2023-04-20 15:49:35 +08:00
|
|
|
)
|
2013-02-07 12:15:48 +08:00
|
|
|
end
|
|
|
|
|
2019-01-04 01:03:01 +08:00
|
|
|
def reviewable_flag
|
|
|
|
ReviewableFlaggedPost.pending.find_by(target: self)
|
|
|
|
end
|
|
|
|
|
2024-04-10 10:02:44 +08:00
|
|
|
# NOTE (martin): This is turning into hack city; when changing this also
|
|
|
|
# consider how it interacts with UploadSecurity and the uploads.rake tasks.
|
2024-04-09 11:23:11 +08:00
|
|
|
def should_secure_uploads?
|
2022-09-29 07:24:33 +08:00
|
|
|
return false if !SiteSetting.secure_uploads?
|
2023-11-28 11:12:28 +08:00
|
|
|
topic_including_deleted = Topic.with_deleted.find_by(id: self.topic_id)
|
|
|
|
return false if topic_including_deleted.blank?
|
2023-09-06 07:39:09 +08:00
|
|
|
|
2024-04-10 10:02:44 +08:00
|
|
|
# NOTE: This is to be used for plugins where adding a new public upload
|
|
|
|
# type that should not be secured via UploadSecurity.register_custom_public_type
|
|
|
|
# is not an option. This also is not taken into account in the secure upload
|
|
|
|
# rake tasks, and will more than likely change in future.
|
|
|
|
modifier_result =
|
|
|
|
DiscoursePluginRegistry.apply_modifier(
|
|
|
|
:post_should_secure_uploads?,
|
|
|
|
nil,
|
|
|
|
self,
|
|
|
|
topic_including_deleted,
|
|
|
|
)
|
|
|
|
return modifier_result if !modifier_result.nil?
|
|
|
|
|
2023-09-06 07:39:09 +08:00
|
|
|
# NOTE: This is meant to be a stopgap solution to prevent secure uploads
|
|
|
|
# in a single place (private messages) for sensitive admin data exports.
|
|
|
|
# Ideally we would want a more comprehensive way of saying that certain
|
|
|
|
# upload types get secured which is a hybrid/mixed mode secure uploads,
|
|
|
|
# but for now this will do the trick.
|
2023-11-28 11:12:28 +08:00
|
|
|
return topic_including_deleted.private_message? if SiteSetting.secure_uploads_pm_only?
|
2023-09-06 07:39:09 +08:00
|
|
|
|
2023-11-28 11:12:28 +08:00
|
|
|
SiteSetting.login_required? || topic_including_deleted.private_message? ||
|
|
|
|
topic_including_deleted.read_restricted_category?
|
2019-11-18 09:25:42 +08:00
|
|
|
end
|
|
|
|
|
2021-03-11 19:21:24 +08:00
|
|
|
def hide!(post_action_type_id, reason = nil, custom_message: nil)
|
2019-01-04 01:03:01 +08:00
|
|
|
return if hidden?
|
|
|
|
|
|
|
|
reason ||=
|
2023-01-09 20:20:10 +08:00
|
|
|
(
|
2019-01-04 01:03:01 +08:00
|
|
|
if hidden_at
|
|
|
|
Post.hidden_reasons[:flag_threshold_reached_again]
|
2023-01-09 20:20:10 +08:00
|
|
|
else
|
2019-01-04 01:03:01 +08:00
|
|
|
Post.hidden_reasons[:flag_threshold_reached]
|
2023-01-09 20:20:10 +08:00
|
|
|
end
|
|
|
|
)
|
2019-01-04 01:03:01 +08:00
|
|
|
|
|
|
|
hiding_again = hidden_at.present?
|
|
|
|
|
2022-02-11 09:00:58 +08:00
|
|
|
Post.transaction do
|
2023-08-18 10:55:17 +08:00
|
|
|
self.skip_validation = true
|
2024-04-29 08:34:46 +08:00
|
|
|
should_update_user_stat = true
|
2023-08-18 10:55:17 +08:00
|
|
|
|
|
|
|
update!(hidden: true, hidden_at: Time.zone.now, hidden_reason_id: reason)
|
2022-02-11 09:00:58 +08:00
|
|
|
|
2024-04-29 08:34:46 +08:00
|
|
|
any_visible_posts_in_topic =
|
|
|
|
Post.exists?(topic_id: topic_id, hidden: false, post_type: Post.types[:regular])
|
|
|
|
|
|
|
|
if !any_visible_posts_in_topic
|
|
|
|
self.topic.update_status(
|
|
|
|
"visible",
|
|
|
|
false,
|
|
|
|
Discourse.system_user,
|
|
|
|
{ visibility_reason_id: Topic.visibility_reasons[:op_flag_threshold_reached] },
|
|
|
|
)
|
|
|
|
should_update_user_stat = false
|
|
|
|
end
|
2022-02-11 09:00:58 +08:00
|
|
|
|
2024-04-29 08:34:46 +08:00
|
|
|
# We need to do this because TopicStatusUpdater also does the decrement
|
|
|
|
# and we don't want to double count for the OP.
|
|
|
|
UserStatCountUpdater.decrement!(self) if should_update_user_stat
|
2022-02-11 09:00:58 +08:00
|
|
|
end
|
2019-01-04 01:03:01 +08:00
|
|
|
|
|
|
|
# inform user
|
|
|
|
if user.present?
|
|
|
|
options = {
|
|
|
|
url: url,
|
|
|
|
edit_delay: SiteSetting.cooldown_minutes_after_hiding_posts,
|
|
|
|
flag_reason:
|
|
|
|
I18n.t(
|
FIX: serialize Flags instead of PostActionType (#28362)
### Why?
Before, all flags were static. Therefore, they were stored in class variables and serialized by SiteSerializer. Recently, we added an option for admins to add their own flags or disable existing flags. Therefore, the class variable had to be dropped because it was unsafe for a multisite environment. However, it started causing performance problems.
### Solution
When a new Flag system is used, instead of using PostActionType, we can serialize Flags and use fragment cache for performance reasons.
At the same time, we are still supporting deprecated `replace_flags` API call. When it is used, we fall back to the old solution and the admin cannot add custom flags. In a couple of months, we will be able to drop that API function and clean that code properly. However, because it may still be used, redis cache was introduced to improve performance.
To test backward compatibility you can add this code to any plugin
```ruby
replace_flags do |flag_settings|
flag_settings.add(
4,
:inappropriate,
topic_type: true,
notify_type: true,
auto_action_type: true,
)
flag_settings.add(1001, :trolling, topic_type: true, notify_type: true, auto_action_type: true)
end
```
2024-08-14 10:13:46 +08:00
|
|
|
"flag_reasons.#{post_action_type_view.types[post_action_type_id]}",
|
2019-01-04 01:03:01 +08:00
|
|
|
locale: SiteSetting.default_locale,
|
|
|
|
base_path: Discourse.base_path,
|
2024-12-04 11:46:52 +08:00
|
|
|
default: PostActionType.names[post_action_type_id],
|
2019-01-04 01:03:01 +08:00
|
|
|
),
|
|
|
|
}
|
|
|
|
|
2021-03-11 19:21:24 +08:00
|
|
|
message = custom_message
|
|
|
|
message = hiding_again ? :post_hidden_again : :post_hidden if message.nil?
|
|
|
|
|
2019-01-04 01:03:01 +08:00
|
|
|
Jobs.enqueue_in(
|
|
|
|
5.seconds,
|
|
|
|
:send_system_message,
|
|
|
|
user_id: user.id,
|
2022-02-08 04:18:17 +08:00
|
|
|
message_type: message.to_s,
|
2019-01-04 01:03:01 +08:00
|
|
|
message_options: options,
|
|
|
|
)
|
|
|
|
end
|
2016-03-31 01:27:34 +08:00
|
|
|
end
|
|
|
|
|
2013-02-07 12:15:48 +08:00
|
|
|
def unhide!
|
2022-02-11 09:00:58 +08:00
|
|
|
Post.transaction do
|
|
|
|
self.update!(hidden: false)
|
2024-04-29 08:34:46 +08:00
|
|
|
should_update_user_stat = true
|
|
|
|
|
|
|
|
# NOTE: We have to consider `nil` a valid reason here because historically
|
|
|
|
# topics didn't have a visibility_reason_id, if we didn't do this we would
|
|
|
|
# break backwards compat since we cannot backfill data.
|
|
|
|
hidden_because_of_op_flagging =
|
|
|
|
self.topic.visibility_reason_id == Topic.visibility_reasons[:op_flag_threshold_reached] ||
|
|
|
|
self.topic.visibility_reason_id.nil?
|
|
|
|
|
|
|
|
if is_first_post? && hidden_because_of_op_flagging
|
|
|
|
self.topic.update_status(
|
|
|
|
"visible",
|
|
|
|
true,
|
|
|
|
Discourse.system_user,
|
|
|
|
{ visibility_reason_id: Topic.visibility_reasons[:op_unhidden] },
|
|
|
|
)
|
|
|
|
should_update_user_stat = false
|
|
|
|
end
|
|
|
|
|
|
|
|
# We need to do this because TopicStatusUpdater also does the increment
|
|
|
|
# and we don't want to double count for the OP.
|
|
|
|
UserStatCountUpdater.increment!(self) if should_update_user_stat
|
|
|
|
|
2022-02-11 09:00:58 +08:00
|
|
|
save(validate: false)
|
|
|
|
end
|
|
|
|
|
2014-09-23 00:55:13 +08:00
|
|
|
publish_change_to_clients!(:acted)
|
2013-02-07 12:15:48 +08:00
|
|
|
end
|
|
|
|
|
2024-01-03 14:55:08 +08:00
|
|
|
def full_url(opts = {})
|
|
|
|
"#{Discourse.base_url}#{url(opts)}"
|
2016-01-13 01:38:49 +08:00
|
|
|
end
|
|
|
|
|
2024-01-03 14:55:08 +08:00
|
|
|
def relative_url(opts = {})
|
|
|
|
"#{Discourse.base_path}#{url(opts)}"
|
2023-11-13 11:06:25 +08:00
|
|
|
end
|
|
|
|
|
2017-04-25 03:26:06 +08:00
|
|
|
def url(opts = nil)
|
|
|
|
opts ||= {}
|
|
|
|
|
2015-08-12 05:28:36 +08:00
|
|
|
if topic
|
2017-04-25 03:26:06 +08:00
|
|
|
Post.url(topic.slug, topic.id, post_number, opts)
|
2015-08-12 05:28:36 +08:00
|
|
|
else
|
|
|
|
"/404"
|
|
|
|
end
|
2013-04-22 15:45:03 +08:00
|
|
|
end
|
|
|
|
|
2022-03-15 17:17:06 +08:00
|
|
|
def canonical_url
|
|
|
|
topic_view = TopicView.new(topic, nil, post_number: post_number)
|
|
|
|
|
|
|
|
page = ""
|
|
|
|
|
|
|
|
page = "?page=#{topic_view.page}" if topic_view.page > 1
|
|
|
|
|
|
|
|
"#{topic.url}#{page}#post_#{post_number}"
|
|
|
|
end
|
|
|
|
|
2016-06-17 09:27:52 +08:00
|
|
|
def unsubscribe_url(user)
|
2022-06-22 02:49:47 +08:00
|
|
|
key_value = UnsubscribeKey.create_key_for(user, UnsubscribeKey::TOPIC_TYPE, post: self)
|
|
|
|
|
|
|
|
"#{Discourse.base_url}/email/unsubscribe/#{key_value}"
|
2016-06-17 09:27:52 +08:00
|
|
|
end
|
|
|
|
|
2017-04-25 03:26:06 +08:00
|
|
|
def self.url(slug, topic_id, post_number, opts = nil)
|
|
|
|
opts ||= {}
|
|
|
|
|
2019-05-03 06:17:27 +08:00
|
|
|
result = +"/t/"
|
|
|
|
result << "#{slug}/" if !opts[:without_slug]
|
2017-04-25 03:26:06 +08:00
|
|
|
|
2024-01-03 14:55:08 +08:00
|
|
|
if post_number == 1 && opts[:share_url]
|
|
|
|
"#{result}#{topic_id}"
|
|
|
|
else
|
|
|
|
"#{result}#{topic_id}/#{post_number}"
|
|
|
|
end
|
2013-04-22 15:45:03 +08:00
|
|
|
end
|
|
|
|
|
|
|
|
def self.urls(post_ids)
|
|
|
|
ids = post_ids.map { |u| u }
|
|
|
|
if ids.length > 0
|
|
|
|
urls = {}
|
|
|
|
Topic
|
|
|
|
.joins(:posts)
|
|
|
|
.where("posts.id" => ids)
|
2013-04-24 16:05:35 +08:00
|
|
|
.select(["posts.id as post_id", "post_number", "topics.slug", "topics.title", "topics.id"])
|
|
|
|
.each { |t| urls[t.post_id.to_i] = url(t.slug, t.id, t.post_number) }
|
2013-04-22 15:45:03 +08:00
|
|
|
urls
|
2013-04-24 16:05:35 +08:00
|
|
|
else
|
2013-04-22 15:45:03 +08:00
|
|
|
{}
|
|
|
|
end
|
2013-02-06 03:16:51 +08:00
|
|
|
end
|
|
|
|
|
2014-10-28 05:06:43 +08:00
|
|
|
def revise(updated_by, changes = {}, opts = {})
|
|
|
|
PostRevisor.new(self).revise!(updated_by, changes, opts)
|
2013-02-09 23:33:07 +08:00
|
|
|
end
|
|
|
|
|
2019-01-17 11:53:09 +08:00
|
|
|
def self.rebake_old(limit, priority: :normal, rate_limiter: true)
|
2019-01-04 06:24:46 +08:00
|
|
|
limiter =
|
|
|
|
RateLimiter.new(
|
|
|
|
nil,
|
|
|
|
"global_periodical_rebake_limit",
|
|
|
|
GlobalSetting.max_old_rebakes_per_15_minutes,
|
|
|
|
900,
|
|
|
|
global: true,
|
|
|
|
)
|
|
|
|
|
2014-07-18 04:22:46 +08:00
|
|
|
problems = []
|
2014-05-30 12:45:39 +08:00
|
|
|
Post
|
|
|
|
.where("baked_version IS NULL OR baked_version < ?", BAKED_VERSION)
|
2017-12-15 07:28:07 +08:00
|
|
|
.order("id desc")
|
2018-01-05 06:53:46 +08:00
|
|
|
.limit(limit)
|
|
|
|
.pluck(:id)
|
|
|
|
.each do |id|
|
2019-01-04 06:24:46 +08:00
|
|
|
begin
|
2019-01-17 11:53:09 +08:00
|
|
|
break if !limiter.can_perform?
|
2019-01-04 06:24:46 +08:00
|
|
|
|
2018-01-05 06:53:46 +08:00
|
|
|
post = Post.find(id)
|
|
|
|
post.rebake!(priority: priority)
|
2017-12-27 09:44:41 +08:00
|
|
|
|
2023-01-09 20:20:10 +08:00
|
|
|
begin
|
2018-01-05 06:53:46 +08:00
|
|
|
limiter.performed! if rate_limiter
|
|
|
|
rescue RateLimiter::LimitExceeded
|
|
|
|
break
|
2023-01-09 20:20:10 +08:00
|
|
|
end
|
2018-01-05 06:53:46 +08:00
|
|
|
rescue => e
|
|
|
|
problems << { post: post, ex: e }
|
2017-12-27 10:51:16 +08:00
|
|
|
|
2018-01-05 06:53:46 +08:00
|
|
|
attempts = post.custom_fields["rebake_attempts"].to_i
|
2017-12-27 09:44:41 +08:00
|
|
|
|
|
|
|
if attempts > 3
|
2018-01-05 06:53:46 +08:00
|
|
|
post.update_columns(baked_version: BAKED_VERSION)
|
2018-12-20 00:47:37 +08:00
|
|
|
Discourse.warn_exception(
|
2023-01-09 20:20:10 +08:00
|
|
|
e,
|
2018-12-20 00:47:37 +08:00
|
|
|
message: "Can not rebake post# #{post.id} after 3 attempts, giving up",
|
2023-01-09 20:20:10 +08:00
|
|
|
)
|
|
|
|
else
|
2018-01-05 06:53:46 +08:00
|
|
|
post.custom_fields["rebake_attempts"] = attempts + 1
|
|
|
|
post.save_custom_fields
|
2023-01-09 20:20:10 +08:00
|
|
|
end
|
|
|
|
end
|
2014-05-28 10:30:43 +08:00
|
|
|
end
|
2014-07-18 04:22:46 +08:00
|
|
|
problems
|
2014-05-28 10:30:43 +08:00
|
|
|
end
|
|
|
|
|
2023-10-19 07:48:01 +08:00
|
|
|
def rebake!(invalidate_broken_images: false, invalidate_oneboxes: false, priority: nil)
|
2019-01-09 05:57:20 +08:00
|
|
|
new_cooked = cook(raw, topic_id: topic_id, invalidate_oneboxes: invalidate_oneboxes)
|
2014-05-28 10:30:43 +08:00
|
|
|
old_cooked = cooked
|
|
|
|
|
2019-04-01 10:14:29 +08:00
|
|
|
update_columns(cooked: new_cooked, baked_at: Time.zone.now, baked_version: BAKED_VERSION)
|
2014-05-28 10:30:43 +08:00
|
|
|
|
2020-06-01 13:04:16 +08:00
|
|
|
topic&.update_excerpt(excerpt_for_topic) if is_first_post?
|
2020-05-23 12:56:13 +08:00
|
|
|
|
2019-01-09 05:57:20 +08:00
|
|
|
if invalidate_broken_images
|
2022-05-03 20:53:32 +08:00
|
|
|
post_hotlinked_media.download_failed.destroy_all
|
|
|
|
post_hotlinked_media.upload_create_failed.destroy_all
|
2018-12-27 01:52:07 +08:00
|
|
|
end
|
|
|
|
|
2014-05-28 10:30:43 +08:00
|
|
|
# Extracts urls from the body
|
2014-07-15 15:47:24 +08:00
|
|
|
TopicLink.extract_from(self)
|
|
|
|
QuotedPost.extract_from(self)
|
|
|
|
|
2014-05-28 10:30:43 +08:00
|
|
|
# make sure we trigger the post process
|
2019-01-09 05:57:20 +08:00
|
|
|
trigger_post_process(bypass_bump: true, priority: priority)
|
2014-05-28 10:30:43 +08:00
|
|
|
|
2014-09-23 00:55:13 +08:00
|
|
|
publish_change_to_clients!(:rebaked)
|
|
|
|
|
2014-05-28 10:30:43 +08:00
|
|
|
new_cooked != old_cooked
|
|
|
|
end
|
|
|
|
|
2016-08-20 01:13:22 +08:00
|
|
|
def set_owner(new_user, actor, skip_revision = false)
|
2014-10-28 05:06:43 +08:00
|
|
|
return if user_id == new_user.id
|
|
|
|
|
2018-08-20 18:26:19 +08:00
|
|
|
edit_reason = I18n.t("change_owner.post_revision_text", locale: SiteSetting.default_locale)
|
2017-09-14 22:15:07 +08:00
|
|
|
|
|
|
|
revise(
|
|
|
|
actor,
|
|
|
|
{ raw: self.raw, user_id: new_user.id, edit_reason: edit_reason },
|
2018-02-27 22:46:20 +08:00
|
|
|
bypass_bump: true,
|
|
|
|
skip_revision: skip_revision,
|
|
|
|
skip_validations: true,
|
2014-10-28 05:06:43 +08:00
|
|
|
)
|
2016-03-16 20:49:27 +08:00
|
|
|
|
|
|
|
topic.update_columns(last_post_user_id: new_user.id) if post_number == topic.highest_post_number
|
2014-03-28 09:28:14 +08:00
|
|
|
end
|
|
|
|
|
2013-06-10 00:48:44 +08:00
|
|
|
before_create { PostCreator.before_create_tasks(self) }
|
2013-02-06 03:16:51 +08:00
|
|
|
|
2016-03-29 15:50:17 +08:00
|
|
|
def self.estimate_posts_per_day
|
2019-12-03 17:05:53 +08:00
|
|
|
val = Discourse.redis.get("estimated_posts_per_day")
|
2016-03-29 15:50:17 +08:00
|
|
|
return val.to_i if val
|
|
|
|
|
|
|
|
posts_per_day =
|
|
|
|
Topic.listable_topics.secured.joins(:posts).merge(Post.created_since(30.days.ago)).count / 30
|
2019-12-03 17:05:53 +08:00
|
|
|
Discourse.redis.setex("estimated_posts_per_day", 1.day.to_i, posts_per_day.to_s)
|
2016-03-29 15:50:17 +08:00
|
|
|
posts_per_day
|
|
|
|
end
|
|
|
|
|
2013-02-07 23:45:24 +08:00
|
|
|
before_save do
|
2013-03-01 02:54:12 +08:00
|
|
|
self.last_editor_id ||= user_id
|
2016-10-24 12:02:38 +08:00
|
|
|
|
2021-10-12 14:31:18 +08:00
|
|
|
if will_save_change_to_raw?
|
|
|
|
self.cooked = cook(raw, topic_id: topic_id) if !new_record?
|
|
|
|
self.baked_at = Time.zone.now
|
|
|
|
self.baked_version = BAKED_VERSION
|
2016-10-24 12:02:38 +08:00
|
|
|
end
|
2013-02-06 03:16:51 +08:00
|
|
|
end
|
|
|
|
|
2013-03-19 03:12:31 +08:00
|
|
|
def advance_draft_sequence
|
|
|
|
return if topic.blank? # could be deleted
|
2018-07-11 15:06:49 +08:00
|
|
|
DraftSequence.next!(last_editor_id, topic.draft_key) if last_editor_id
|
2013-03-19 03:12:31 +08:00
|
|
|
end
|
|
|
|
|
2013-07-23 04:39:20 +08:00
|
|
|
# TODO: move to post-analyzer?
|
2013-03-19 03:54:08 +08:00
|
|
|
# Determine what posts are quoted by this post
|
2013-02-06 03:16:51 +08:00
|
|
|
def extract_quoted_post_numbers
|
2013-05-23 03:45:31 +08:00
|
|
|
temp_collector = []
|
2013-02-06 03:16:51 +08:00
|
|
|
|
|
|
|
# Create relationships for the quotes
|
2013-05-23 03:38:45 +08:00
|
|
|
raw
|
|
|
|
.scan(/\[quote=\"([^"]+)"\]/)
|
|
|
|
.each do |quote|
|
|
|
|
args = parse_quote_into_arguments(quote)
|
2013-05-23 03:45:31 +08:00
|
|
|
# If the topic attribute is present, ensure it's the same topic
|
2018-07-10 16:17:28 +08:00
|
|
|
if !(args[:topic].present? && topic_id != args[:topic]) && args[:post] != post_number
|
|
|
|
temp_collector << args[:post]
|
2023-01-09 20:20:10 +08:00
|
|
|
end
|
2018-07-10 16:17:28 +08:00
|
|
|
end
|
2013-02-07 23:45:24 +08:00
|
|
|
|
2013-05-23 03:45:31 +08:00
|
|
|
temp_collector.uniq!
|
|
|
|
self.quoted_post_numbers = temp_collector
|
|
|
|
self.quote_count = temp_collector.size
|
2013-02-06 03:16:51 +08:00
|
|
|
end
|
|
|
|
|
2013-03-19 03:54:08 +08:00
|
|
|
def save_reply_relationships
|
2013-05-24 00:09:06 +08:00
|
|
|
add_to_quoted_post_numbers(reply_to_post_number)
|
|
|
|
return if self.quoted_post_numbers.blank?
|
2013-03-19 03:54:08 +08:00
|
|
|
|
|
|
|
# Create a reply relationship between quoted posts and this new post
|
2013-05-24 00:09:06 +08:00
|
|
|
self.quoted_post_numbers.each do |p|
|
2014-05-06 21:41:59 +08:00
|
|
|
post = Post.find_by(topic_id: topic_id, post_number: p)
|
2013-05-24 00:09:06 +08:00
|
|
|
create_reply_relationship_with(post)
|
2013-03-19 03:54:08 +08:00
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2013-03-19 01:55:34 +08:00
|
|
|
# Enqueue post processing for this post
|
2020-05-29 20:07:47 +08:00
|
|
|
def trigger_post_process(
|
|
|
|
bypass_bump: false,
|
|
|
|
priority: :normal,
|
|
|
|
new_post: false,
|
|
|
|
skip_pull_hotlinked_images: false
|
|
|
|
)
|
2013-11-22 08:52:26 +08:00
|
|
|
args = {
|
2018-09-06 09:58:01 +08:00
|
|
|
bypass_bump: bypass_bump,
|
2020-06-24 17:54:54 +08:00
|
|
|
cooking_options: self.cooking_options,
|
2019-01-17 10:24:32 +08:00
|
|
|
new_post: new_post,
|
2023-11-27 10:38:52 +08:00
|
|
|
post_id: self.id,
|
2020-05-29 20:07:47 +08:00
|
|
|
skip_pull_hotlinked_images: skip_pull_hotlinked_images,
|
2013-11-22 08:52:26 +08:00
|
|
|
}
|
2019-01-09 05:57:20 +08:00
|
|
|
|
2020-06-24 17:54:54 +08:00
|
|
|
args[:image_sizes] = image_sizes if self.image_sizes.present?
|
|
|
|
args[:invalidate_oneboxes] = true if self.invalidate_oneboxes.present?
|
|
|
|
args[:queue] = priority.to_s if priority && priority != :normal
|
2019-01-09 05:57:20 +08:00
|
|
|
|
2013-02-07 23:45:24 +08:00
|
|
|
Jobs.enqueue(:process_post, args)
|
2015-09-04 11:35:25 +08:00
|
|
|
DiscourseEvent.trigger(:after_trigger_post_process, self)
|
2013-02-06 03:16:51 +08:00
|
|
|
end
|
2013-03-08 00:07:59 +08:00
|
|
|
|
2020-04-22 16:52:50 +08:00
|
|
|
def self.public_posts_count_per_day(
|
|
|
|
start_date,
|
|
|
|
end_date,
|
|
|
|
category_id = nil,
|
2023-09-05 13:47:18 +08:00
|
|
|
include_subcategories = false,
|
|
|
|
group_ids = nil
|
2020-04-22 16:52:50 +08:00
|
|
|
)
|
|
|
|
result =
|
|
|
|
public_posts.where(
|
|
|
|
"posts.created_at >= ? AND posts.created_at <= ?",
|
|
|
|
start_date,
|
|
|
|
end_date,
|
2017-11-03 06:24:43 +08:00
|
|
|
).where(post_type: Post.types[:regular])
|
2020-04-22 16:52:50 +08:00
|
|
|
|
|
|
|
if category_id
|
|
|
|
if include_subcategories
|
|
|
|
result = result.where("topics.category_id IN (?)", Category.subcategory_ids(category_id))
|
|
|
|
else
|
2023-09-27 00:26:47 +08:00
|
|
|
result = result.where("topics.category_id IN (?)", category_id)
|
2020-04-22 16:52:50 +08:00
|
|
|
end
|
|
|
|
end
|
2023-09-05 13:47:18 +08:00
|
|
|
if group_ids
|
|
|
|
result =
|
|
|
|
result
|
|
|
|
.joins("INNER JOIN users ON users.id = posts.user_id")
|
|
|
|
.joins("INNER JOIN group_users ON group_users.user_id = users.id")
|
|
|
|
.where("group_users.group_id IN (?)", group_ids)
|
|
|
|
end
|
2020-04-22 16:52:50 +08:00
|
|
|
|
2018-06-05 15:29:17 +08:00
|
|
|
result.group("date(posts.created_at)").order("date(posts.created_at)").count
|
2013-04-04 01:25:52 +08:00
|
|
|
end
|
|
|
|
|
2016-04-21 17:22:41 +08:00
|
|
|
def self.private_messages_count_per_day(start_date, end_date, topic_subtype)
|
2018-06-05 15:29:17 +08:00
|
|
|
private_posts
|
|
|
|
.with_topic_subtype(topic_subtype)
|
|
|
|
.where("posts.created_at >= ? AND posts.created_at <= ?", start_date, end_date)
|
|
|
|
.group("date(posts.created_at)")
|
|
|
|
.order("date(posts.created_at)")
|
|
|
|
.count
|
2013-03-08 00:07:59 +08:00
|
|
|
end
|
2013-05-18 00:15:21 +08:00
|
|
|
|
2024-11-06 06:27:49 +08:00
|
|
|
MAX_REPLY_LEVEL = 1000
|
2017-12-15 07:23:51 +08:00
|
|
|
|
2018-04-21 05:05:51 +08:00
|
|
|
def reply_ids(guardian = nil, only_replies_to_single_post: true)
|
2018-06-20 15:48:02 +08:00
|
|
|
builder = DB.build(<<~SQL)
|
2017-12-14 07:43:48 +08:00
|
|
|
WITH RECURSIVE breadcrumb(id, level) AS (
|
|
|
|
SELECT :post_id, 0
|
2017-12-14 05:12:06 +08:00
|
|
|
UNION
|
2020-01-18 00:24:49 +08:00
|
|
|
SELECT reply_post_id, level + 1
|
2018-04-21 05:05:51 +08:00
|
|
|
FROM post_replies AS r
|
2020-02-04 02:12:27 +08:00
|
|
|
JOIN posts AS p ON p.id = reply_post_id
|
2018-04-21 05:05:51 +08:00
|
|
|
JOIN breadcrumb AS b ON (r.post_id = b.id)
|
2020-01-18 00:24:49 +08:00
|
|
|
WHERE r.post_id <> r.reply_post_id
|
2020-02-04 02:12:27 +08:00
|
|
|
AND b.level < :max_reply_level
|
|
|
|
AND p.topic_id = :topic_id
|
2017-12-14 07:43:48 +08:00
|
|
|
), breadcrumb_with_count AS (
|
2018-04-21 05:05:51 +08:00
|
|
|
SELECT
|
|
|
|
id,
|
|
|
|
level,
|
|
|
|
COUNT(*) AS count
|
|
|
|
FROM post_replies AS r
|
2020-01-18 00:24:49 +08:00
|
|
|
JOIN breadcrumb AS b ON (r.reply_post_id = b.id)
|
|
|
|
WHERE r.reply_post_id <> r.post_id
|
2018-04-21 05:05:51 +08:00
|
|
|
GROUP BY id, level
|
2017-12-14 07:43:48 +08:00
|
|
|
)
|
2020-02-04 02:34:35 +08:00
|
|
|
SELECT id, MIN(level) AS level
|
2018-04-21 05:05:51 +08:00
|
|
|
FROM breadcrumb_with_count
|
|
|
|
/*where*/
|
2020-02-04 02:34:35 +08:00
|
|
|
GROUP BY id
|
2018-04-21 05:05:51 +08:00
|
|
|
ORDER BY id
|
|
|
|
SQL
|
2017-12-14 05:12:06 +08:00
|
|
|
|
2018-04-21 05:05:51 +08:00
|
|
|
builder.where("level > 0")
|
|
|
|
|
|
|
|
# ignore posts that aren't replies to exactly one post
|
|
|
|
# for example it skips a post when it contains 2 quotes (which are replies) from different posts
|
|
|
|
builder.where("count = 1") if only_replies_to_single_post
|
|
|
|
|
2020-02-04 02:12:27 +08:00
|
|
|
replies = builder.query_hash(post_id: id, max_reply_level: MAX_REPLY_LEVEL, topic_id: topic_id)
|
2018-06-20 15:48:02 +08:00
|
|
|
replies.each { |r| r.symbolize_keys! }
|
2017-12-14 05:12:06 +08:00
|
|
|
|
|
|
|
secured_ids = Post.secured(guardian).where(id: replies.map { |r| r[:id] }).pluck(:id).to_set
|
|
|
|
|
2017-12-14 07:43:48 +08:00
|
|
|
replies.reject { |r| !secured_ids.include?(r[:id]) }
|
2017-12-14 05:12:06 +08:00
|
|
|
end
|
|
|
|
|
2013-12-12 10:41:34 +08:00
|
|
|
def revert_to(number)
|
|
|
|
return if number >= version
|
2014-05-06 21:41:59 +08:00
|
|
|
post_revision = PostRevision.find_by(post_id: id, number: (number + 1))
|
2013-12-12 10:41:34 +08:00
|
|
|
post_revision.modifications.each do |attribute, change|
|
|
|
|
attribute = "version" if attribute == "cached_version"
|
|
|
|
write_attribute(attribute, change[0])
|
|
|
|
end
|
|
|
|
end
|
2013-05-18 00:15:21 +08:00
|
|
|
|
2015-04-24 17:14:10 +08:00
|
|
|
def self.rebake_all_quoted_posts(user_id)
|
|
|
|
return if user_id.blank?
|
|
|
|
|
2018-06-19 14:13:14 +08:00
|
|
|
DB.exec(<<~SQL, user_id)
|
2015-04-24 17:14:10 +08:00
|
|
|
WITH user_quoted_posts AS (
|
|
|
|
SELECT post_id
|
|
|
|
FROM quoted_posts
|
2018-06-19 14:13:14 +08:00
|
|
|
WHERE quoted_post_id IN (SELECT id FROM posts WHERE user_id = ?)
|
2015-04-24 17:14:10 +08:00
|
|
|
)
|
|
|
|
UPDATE posts
|
|
|
|
SET baked_version = NULL
|
|
|
|
WHERE baked_version IS NOT NULL
|
|
|
|
AND id IN (SELECT post_id FROM user_quoted_posts)
|
|
|
|
SQL
|
|
|
|
end
|
|
|
|
|
2016-01-27 09:19:49 +08:00
|
|
|
def seen?(user)
|
|
|
|
PostTiming.where(topic_id: topic_id, post_number: post_number, user_id: user.id).exists?
|
|
|
|
end
|
|
|
|
|
2016-12-22 10:13:14 +08:00
|
|
|
def index_search
|
2020-08-21 07:52:43 +08:00
|
|
|
Scheduler::Defer.later "Index post for search" do
|
|
|
|
SearchIndexer.index(self)
|
|
|
|
end
|
2016-12-22 10:13:14 +08:00
|
|
|
end
|
|
|
|
|
2018-01-26 04:38:40 +08:00
|
|
|
def locked?
|
|
|
|
locked_by_id.present?
|
|
|
|
end
|
|
|
|
|
2018-09-06 09:58:01 +08:00
|
|
|
def link_post_uploads(fragments: nil)
|
|
|
|
upload_ids = []
|
|
|
|
|
2019-05-04 03:46:20 +08:00
|
|
|
each_upload_url(fragments: fragments) do |src, _, sha1|
|
|
|
|
upload = nil
|
|
|
|
upload = Upload.find_by(sha1: sha1) if sha1.present?
|
|
|
|
upload ||= Upload.get_from_url(src)
|
2023-03-10 00:26:47 +08:00
|
|
|
|
|
|
|
# Link any video thumbnails
|
2023-04-29 04:08:20 +08:00
|
|
|
if SiteSetting.video_thumbnails_enabled && upload.present? &&
|
|
|
|
FileHelper.supported_video.include?(upload.extension&.downcase)
|
2023-03-10 00:26:47 +08:00
|
|
|
# Video thumbnails have the filename of the video file sha1 with a .png or .jpg extension.
|
|
|
|
# This is because at time of upload in the composer we don't know the topic/post id yet
|
|
|
|
# and there is no thumbnail info added to the markdown to tie the thumbnail to the topic/post after
|
|
|
|
# creation.
|
|
|
|
thumbnail =
|
2023-05-23 23:00:09 +08:00
|
|
|
Upload
|
|
|
|
.where("original_filename like ?", "#{upload.sha1}.%")
|
|
|
|
.order(id: :desc)
|
|
|
|
.first if upload.sha1.present?
|
2024-02-15 04:43:53 +08:00
|
|
|
if thumbnail.present?
|
2023-04-22 03:33:33 +08:00
|
|
|
upload_ids << thumbnail.id
|
2024-02-15 04:43:53 +08:00
|
|
|
if self.is_first_post? && !self.topic.image_upload_id
|
|
|
|
self.topic.update_column(:image_upload_id, thumbnail.id)
|
|
|
|
extra_sizes =
|
|
|
|
ThemeModifierHelper.new(
|
|
|
|
theme_ids: Theme.user_selectable.pluck(:id),
|
|
|
|
).topic_thumbnail_sizes
|
|
|
|
self.topic.generate_thumbnails!(extra_sizes: extra_sizes)
|
|
|
|
end
|
2023-03-10 00:26:47 +08:00
|
|
|
end
|
|
|
|
end
|
2019-05-04 03:46:20 +08:00
|
|
|
upload_ids << upload.id if upload.present?
|
2018-09-06 09:58:01 +08:00
|
|
|
end
|
|
|
|
|
2022-06-09 07:24:30 +08:00
|
|
|
upload_references =
|
|
|
|
upload_ids.map do |upload_id|
|
|
|
|
{
|
|
|
|
target_id: self.id,
|
|
|
|
target_type: self.class.name,
|
|
|
|
upload_id: upload_id,
|
|
|
|
created_at: Time.zone.now,
|
|
|
|
updated_at: Time.zone.now,
|
|
|
|
}
|
2019-11-18 09:25:42 +08:00
|
|
|
end
|
2018-09-06 09:58:01 +08:00
|
|
|
|
2022-06-09 07:24:30 +08:00
|
|
|
UploadReference.transaction do
|
|
|
|
UploadReference.where(target: self).delete_all
|
|
|
|
UploadReference.insert_all(upload_references) if upload_references.size > 0
|
2020-01-16 11:50:27 +08:00
|
|
|
|
2022-09-29 07:24:33 +08:00
|
|
|
if SiteSetting.secure_uploads?
|
2024-04-16 12:10:25 +08:00
|
|
|
Upload
|
|
|
|
.where(id: upload_ids, access_control_post_id: nil)
|
|
|
|
.where("id NOT IN (SELECT upload_id FROM custom_emojis)")
|
|
|
|
.update_all(access_control_post_id: self.id)
|
2018-09-06 09:58:01 +08:00
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2021-01-29 07:03:44 +08:00
|
|
|
def update_uploads_secure_status(source:)
|
2023-10-19 07:48:01 +08:00
|
|
|
if Discourse.store.external?
|
|
|
|
self.uploads.each { |upload| upload.update_secure_status(source: source) }
|
2019-11-18 09:25:42 +08:00
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2019-07-29 14:35:34 +08:00
|
|
|
def each_upload_url(fragments: nil, include_local_upload: true)
|
2019-06-25 03:49:58 +08:00
|
|
|
current_db = RailsMultisite::ConnectionManagement.current_db
|
2024-05-23 13:15:16 +08:00
|
|
|
|
2019-05-04 03:46:20 +08:00
|
|
|
upload_patterns = [
|
2019-06-25 03:49:58 +08:00
|
|
|
%r{/uploads/#{current_db}/},
|
2019-05-04 03:46:20 +08:00
|
|
|
%r{/original/},
|
2019-05-29 09:00:25 +08:00
|
|
|
%r{/optimized/},
|
2019-06-19 09:10:50 +08:00
|
|
|
%r{/uploads/short-url/[a-zA-Z0-9]+(\.[a-z0-9]+)?},
|
2019-05-04 03:46:20 +08:00
|
|
|
]
|
2019-05-29 09:00:25 +08:00
|
|
|
|
2020-05-05 11:46:57 +08:00
|
|
|
fragments ||= Nokogiri::HTML5.fragment(self.cooked)
|
2024-05-23 13:15:16 +08:00
|
|
|
|
2023-11-27 10:38:52 +08:00
|
|
|
selectors =
|
|
|
|
fragments.css(
|
|
|
|
"a/@href",
|
|
|
|
"img/@src",
|
|
|
|
"source/@src",
|
|
|
|
"track/@src",
|
|
|
|
"video/@poster",
|
|
|
|
"div/@data-video-src",
|
|
|
|
)
|
2019-11-18 09:25:42 +08:00
|
|
|
|
2020-03-10 21:01:40 +08:00
|
|
|
links =
|
|
|
|
selectors
|
|
|
|
.map do |media|
|
2019-09-02 17:41:22 +08:00
|
|
|
src = media.value
|
|
|
|
next if src.blank?
|
2023-01-09 20:20:10 +08:00
|
|
|
|
2019-09-02 17:41:22 +08:00
|
|
|
if src.end_with?("/images/transparent.png") &&
|
|
|
|
(parent = media.parent)["data-orig-src"].present?
|
|
|
|
parent["data-orig-src"]
|
|
|
|
else
|
|
|
|
src
|
2023-01-09 20:20:10 +08:00
|
|
|
end
|
2019-09-02 17:41:22 +08:00
|
|
|
end
|
|
|
|
.compact
|
|
|
|
.uniq
|
2019-05-04 03:46:20 +08:00
|
|
|
|
|
|
|
links.each do |src|
|
2019-09-02 17:41:22 +08:00
|
|
|
src = src.split("?")[0]
|
|
|
|
|
|
|
|
if src.start_with?("upload://")
|
|
|
|
sha1 = Upload.sha1_from_short_url(src)
|
|
|
|
yield(src, nil, sha1)
|
|
|
|
next
|
2024-05-23 13:15:16 +08:00
|
|
|
end
|
|
|
|
|
|
|
|
if src.include?("/uploads/short-url/")
|
|
|
|
host =
|
|
|
|
begin
|
|
|
|
URI(src).host
|
|
|
|
rescue URI::Error
|
|
|
|
end
|
|
|
|
|
|
|
|
next if host.present? && host != Discourse.current_hostname
|
|
|
|
|
2019-09-22 18:02:28 +08:00
|
|
|
sha1 = Upload.sha1_from_short_path(src)
|
|
|
|
yield(src, nil, sha1)
|
|
|
|
next
|
2019-09-02 17:41:22 +08:00
|
|
|
end
|
|
|
|
|
|
|
|
next if upload_patterns.none? { |pattern| src =~ pattern }
|
2019-09-22 18:02:28 +08:00
|
|
|
next if Rails.configuration.multisite && src.exclude?(current_db)
|
2019-05-04 03:46:20 +08:00
|
|
|
|
|
|
|
src = "#{SiteSetting.force_https ? "https" : "http"}:#{src}" if src.start_with?("//")
|
2024-05-23 13:15:16 +08:00
|
|
|
|
2023-02-16 17:40:11 +08:00
|
|
|
if !Discourse.store.has_been_uploaded?(src) && !Upload.secure_uploads_url?(src) &&
|
|
|
|
!(include_local_upload && src =~ %r{\A/[^/]}i)
|
2022-09-29 07:24:33 +08:00
|
|
|
next
|
2019-05-04 03:46:20 +08:00
|
|
|
end
|
|
|
|
|
2023-01-09 20:20:10 +08:00
|
|
|
path =
|
|
|
|
begin
|
|
|
|
URI(
|
2019-12-12 10:49:21 +08:00
|
|
|
UrlHelper.unencode(GlobalSetting.cdn_url ? src.sub(GlobalSetting.cdn_url, "") : src),
|
2023-01-09 20:20:10 +08:00
|
|
|
)&.path
|
2019-05-04 03:46:20 +08:00
|
|
|
rescue URI::Error
|
2023-01-09 20:20:10 +08:00
|
|
|
end
|
|
|
|
|
2019-05-04 03:46:20 +08:00
|
|
|
next if path.blank?
|
|
|
|
|
|
|
|
sha1 =
|
|
|
|
if path.include? "optimized"
|
|
|
|
OptimizedImage.extract_sha1(path)
|
|
|
|
else
|
2019-05-29 09:00:25 +08:00
|
|
|
Upload.extract_sha1(path) || Upload.sha1_from_short_path(path)
|
2019-05-04 03:46:20 +08:00
|
|
|
end
|
|
|
|
|
|
|
|
yield(src, path, sha1)
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2019-07-29 14:35:34 +08:00
|
|
|
def self.find_missing_uploads(include_local_upload: true)
|
2019-05-04 03:46:20 +08:00
|
|
|
missing_uploads = []
|
|
|
|
missing_post_uploads = {}
|
2019-05-16 17:47:53 +08:00
|
|
|
count = 0
|
2019-05-04 03:46:20 +08:00
|
|
|
|
2019-08-15 09:48:08 +08:00
|
|
|
DistributedMutex.synchronize("find_missing_uploads", validity: 30.minutes) do
|
2019-05-16 17:47:53 +08:00
|
|
|
PostCustomField.where(name: Post::MISSING_UPLOADS).delete_all
|
2024-05-23 13:15:16 +08:00
|
|
|
|
2019-05-16 17:47:53 +08:00
|
|
|
query =
|
|
|
|
Post
|
|
|
|
.have_uploads
|
|
|
|
.joins(:topic)
|
|
|
|
.joins(
|
|
|
|
"LEFT JOIN post_custom_fields ON posts.id = post_custom_fields.post_id AND post_custom_fields.name = '#{Post::MISSING_UPLOADS_IGNORED}'",
|
|
|
|
)
|
|
|
|
.where("post_custom_fields.id IS NULL")
|
|
|
|
.select(:id, :cooked)
|
|
|
|
|
|
|
|
query.find_in_batches do |posts|
|
|
|
|
ids = posts.pluck(:id)
|
2022-06-09 07:24:30 +08:00
|
|
|
sha1s =
|
|
|
|
Upload
|
|
|
|
.joins(:upload_references)
|
|
|
|
.where(upload_references: { target_type: "Post" })
|
|
|
|
.where("upload_references.target_id BETWEEN ? AND ?", ids.min, ids.max)
|
|
|
|
.pluck(:sha1)
|
2019-05-16 17:47:53 +08:00
|
|
|
|
|
|
|
posts.each do |post|
|
|
|
|
post.each_upload_url do |src, path, sha1|
|
|
|
|
next if sha1.present? && sha1s.include?(sha1)
|
|
|
|
|
|
|
|
missing_post_uploads[post.id] ||= []
|
|
|
|
|
|
|
|
if missing_uploads.include?(src)
|
|
|
|
missing_post_uploads[post.id] << src
|
|
|
|
next
|
|
|
|
end
|
|
|
|
|
|
|
|
upload_id = nil
|
2023-02-13 12:39:45 +08:00
|
|
|
upload_id = Upload.where(sha1: sha1).pick(:id) if sha1.present?
|
2019-05-16 17:47:53 +08:00
|
|
|
upload_id ||= yield(post, src, path, sha1)
|
|
|
|
|
2019-07-19 04:14:08 +08:00
|
|
|
if upload_id.blank?
|
2019-05-16 17:47:53 +08:00
|
|
|
missing_uploads << src
|
|
|
|
missing_post_uploads[post.id] << src
|
|
|
|
end
|
2019-05-04 03:46:20 +08:00
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2019-05-16 17:47:53 +08:00
|
|
|
missing_post_uploads =
|
|
|
|
missing_post_uploads.reject do |post_id, uploads|
|
|
|
|
if uploads.present?
|
|
|
|
PostCustomField.create!(
|
|
|
|
post_id: post_id,
|
|
|
|
name: Post::MISSING_UPLOADS,
|
|
|
|
value: uploads.to_json,
|
|
|
|
)
|
|
|
|
count += uploads.count
|
|
|
|
end
|
2019-05-16 12:34:04 +08:00
|
|
|
|
2019-05-16 17:47:53 +08:00
|
|
|
uploads.empty?
|
|
|
|
end
|
2019-05-04 03:46:20 +08:00
|
|
|
end
|
|
|
|
|
2019-05-16 12:34:04 +08:00
|
|
|
{ uploads: missing_uploads, post_uploads: missing_post_uploads, count: count }
|
2019-05-04 03:46:20 +08:00
|
|
|
end
|
|
|
|
|
2020-01-23 10:01:10 +08:00
|
|
|
def owned_uploads_via_access_control
|
|
|
|
Upload.where(access_control_post_id: self.id)
|
|
|
|
end
|
|
|
|
|
FEATURE: Include optimized thumbnails for topics (#9215)
This introduces new APIs for obtaining optimized thumbnails for topics. There are a few building blocks required for this:
- Introduces new `image_upload_id` columns on the `posts` and `topics` table. This replaces the old `image_url` column, which means that thumbnails are now restricted to uploads. Hotlinked thumbnails are no longer possible. In normal use (with pull_hotlinked_images enabled), this has no noticeable impact
- A migration attempts to match existing urls to upload records. If a match cannot be found then the posts will be queued for rebake
- Optimized thumbnails are generated during post_process_cooked. If thumbnails are missing when serializing a topic list, then a sidekiq job is queued
- Topic lists and topics now include a `thumbnails` key, which includes all the available images:
```
"thumbnails": [
{
"max_width": null,
"max_height": null,
"url": "//example.com/original-image.png",
"width": 1380,
"height": 1840
},
{
"max_width": 1024,
"max_height": 1024,
"url": "//example.com/optimized-image.png",
"width": 768,
"height": 1024
}
]
```
- Themes can request additional thumbnail sizes by using a modifier in their `about.json` file:
```
"modifiers": {
"topic_thumbnail_sizes": [
[200, 200],
[800, 800]
],
...
```
Remember that these are generated asynchronously, so your theme should include logic to fallback to other available thumbnails if your requested size has not yet been generated
- Two new raw plugin outlets are introduced, to improve the customisability of the topic list. `topic-list-before-columns` and `topic-list-before-link`
2020-05-05 16:07:50 +08:00
|
|
|
def image_url
|
2021-07-27 00:09:51 +08:00
|
|
|
raw_url = image_upload&.url
|
|
|
|
UrlHelper.cook_url(raw_url, secure: image_upload&.secure?, local: true) if raw_url
|
FEATURE: Include optimized thumbnails for topics (#9215)
This introduces new APIs for obtaining optimized thumbnails for topics. There are a few building blocks required for this:
- Introduces new `image_upload_id` columns on the `posts` and `topics` table. This replaces the old `image_url` column, which means that thumbnails are now restricted to uploads. Hotlinked thumbnails are no longer possible. In normal use (with pull_hotlinked_images enabled), this has no noticeable impact
- A migration attempts to match existing urls to upload records. If a match cannot be found then the posts will be queued for rebake
- Optimized thumbnails are generated during post_process_cooked. If thumbnails are missing when serializing a topic list, then a sidekiq job is queued
- Topic lists and topics now include a `thumbnails` key, which includes all the available images:
```
"thumbnails": [
{
"max_width": null,
"max_height": null,
"url": "//example.com/original-image.png",
"width": 1380,
"height": 1840
},
{
"max_width": 1024,
"max_height": 1024,
"url": "//example.com/optimized-image.png",
"width": 768,
"height": 1024
}
]
```
- Themes can request additional thumbnail sizes by using a modifier in their `about.json` file:
```
"modifiers": {
"topic_thumbnail_sizes": [
[200, 200],
[800, 800]
],
...
```
Remember that these are generated asynchronously, so your theme should include logic to fallback to other available thumbnails if your requested size has not yet been generated
- Two new raw plugin outlets are introduced, to improve the customisability of the topic list. `topic-list-before-columns` and `topic-list-before-link`
2020-05-05 16:07:50 +08:00
|
|
|
end
|
|
|
|
|
2021-10-13 17:53:23 +08:00
|
|
|
def cannot_permanently_delete_reason(user)
|
|
|
|
if self.deleted_by_id == user&.id && self.deleted_at >= Post::PERMANENT_DELETE_TIMER.ago
|
|
|
|
time_left =
|
|
|
|
RateLimiter.time_left(
|
|
|
|
Post::PERMANENT_DELETE_TIMER.to_i - Time.zone.now.to_i + self.deleted_at.to_i,
|
|
|
|
)
|
|
|
|
I18n.t("post.cannot_permanently_delete.wait_or_different_admin", time_left: time_left)
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2022-12-06 23:10:36 +08:00
|
|
|
def mentions
|
|
|
|
PrettyText.extract_mentions(Nokogiri::HTML5.fragment(cooked))
|
|
|
|
end
|
|
|
|
|
2013-12-12 10:41:34 +08:00
|
|
|
private
|
2013-05-23 03:45:31 +08:00
|
|
|
|
2013-05-23 03:38:45 +08:00
|
|
|
def parse_quote_into_arguments(quote)
|
2024-05-27 18:27:13 +08:00
|
|
|
return {} if quote.blank?
|
2014-08-18 11:00:02 +08:00
|
|
|
args = HashWithIndifferentAccess.new
|
2013-05-23 03:38:45 +08:00
|
|
|
quote.first.scan(/([a-z]+)\:(\d+)/).each { |arg| args[arg[0]] = arg[1].to_i }
|
|
|
|
args
|
|
|
|
end
|
2013-05-23 03:45:31 +08:00
|
|
|
|
2013-05-24 00:07:45 +08:00
|
|
|
def add_to_quoted_post_numbers(num)
|
2024-05-27 18:27:13 +08:00
|
|
|
return if num.blank?
|
2013-05-24 00:07:45 +08:00
|
|
|
self.quoted_post_numbers ||= []
|
|
|
|
self.quoted_post_numbers << num
|
|
|
|
end
|
2013-05-24 00:08:24 +08:00
|
|
|
|
|
|
|
def create_reply_relationship_with(post)
|
2018-05-16 23:02:43 +08:00
|
|
|
return if post.nil? || self.deleted_at.present?
|
2020-01-18 00:24:49 +08:00
|
|
|
post_reply = post.post_replies.new(reply_post_id: id)
|
2013-05-24 00:08:24 +08:00
|
|
|
if post_reply.save
|
2015-09-25 08:15:58 +08:00
|
|
|
if Topic.visible_post_types.include?(self.post_type)
|
|
|
|
Post.where(id: post.id).update_all ["reply_count = reply_count + 1"]
|
|
|
|
end
|
2013-05-24 00:08:24 +08:00
|
|
|
end
|
|
|
|
end
|
2013-02-06 03:16:51 +08:00
|
|
|
end
|
2013-05-24 10:48:32 +08:00
|
|
|
|
|
|
|
# == Schema Information
|
|
|
|
#
|
|
|
|
# Table name: posts
|
|
|
|
#
|
|
|
|
# id :integer not null, primary key
|
2013-09-04 05:19:29 +08:00
|
|
|
# user_id :integer
|
2013-05-24 10:48:32 +08:00
|
|
|
# topic_id :integer not null
|
|
|
|
# post_number :integer not null
|
|
|
|
# raw :text not null
|
|
|
|
# cooked :text not null
|
2014-08-27 13:30:17 +08:00
|
|
|
# created_at :datetime not null
|
|
|
|
# updated_at :datetime not null
|
2013-05-24 10:48:32 +08:00
|
|
|
# reply_to_post_number :integer
|
|
|
|
# reply_count :integer default(0), not null
|
|
|
|
# quote_count :integer default(0), not null
|
|
|
|
# deleted_at :datetime
|
|
|
|
# off_topic_count :integer default(0), not null
|
|
|
|
# like_count :integer default(0), not null
|
|
|
|
# incoming_link_count :integer default(0), not null
|
|
|
|
# bookmark_count :integer default(0), not null
|
|
|
|
# score :float
|
|
|
|
# reads :integer default(0), not null
|
|
|
|
# post_type :integer default(1), not null
|
|
|
|
# sort_order :integer
|
|
|
|
# last_editor_id :integer
|
|
|
|
# hidden :boolean default(FALSE), not null
|
|
|
|
# hidden_reason_id :integer
|
|
|
|
# notify_moderators_count :integer default(0), not null
|
|
|
|
# spam_count :integer default(0), not null
|
|
|
|
# illegal_count :integer default(0), not null
|
|
|
|
# inappropriate_count :integer default(0), not null
|
|
|
|
# last_version_at :datetime not null
|
|
|
|
# user_deleted :boolean default(FALSE), not null
|
|
|
|
# reply_to_user_id :integer
|
|
|
|
# percent_rank :float default(1.0)
|
|
|
|
# notify_user_count :integer default(0), not null
|
2013-06-17 08:48:58 +08:00
|
|
|
# like_score :integer default(0), not null
|
2013-07-14 09:24:16 +08:00
|
|
|
# deleted_by_id :integer
|
2019-01-12 03:29:56 +08:00
|
|
|
# edit_reason :string
|
2014-02-07 08:07:36 +08:00
|
|
|
# word_count :integer
|
|
|
|
# version :integer default(1), not null
|
|
|
|
# cook_method :integer default(1), not null
|
2014-05-22 07:00:38 +08:00
|
|
|
# wiki :boolean default(FALSE), not null
|
2014-05-29 12:59:14 +08:00
|
|
|
# baked_at :datetime
|
2014-07-03 15:29:44 +08:00
|
|
|
# baked_version :integer
|
|
|
|
# hidden_at :datetime
|
2014-07-15 09:29:44 +08:00
|
|
|
# self_edits :integer default(0), not null
|
2014-07-31 11:14:40 +08:00
|
|
|
# reply_quoted :boolean default(FALSE), not null
|
2014-11-20 11:53:15 +08:00
|
|
|
# via_email :boolean default(FALSE), not null
|
|
|
|
# raw_email :text
|
|
|
|
# public_version :integer default(1), not null
|
2019-01-12 03:29:56 +08:00
|
|
|
# action_code :string
|
2018-02-20 14:28:58 +08:00
|
|
|
# locked_by_id :integer
|
FEATURE: Include optimized thumbnails for topics (#9215)
This introduces new APIs for obtaining optimized thumbnails for topics. There are a few building blocks required for this:
- Introduces new `image_upload_id` columns on the `posts` and `topics` table. This replaces the old `image_url` column, which means that thumbnails are now restricted to uploads. Hotlinked thumbnails are no longer possible. In normal use (with pull_hotlinked_images enabled), this has no noticeable impact
- A migration attempts to match existing urls to upload records. If a match cannot be found then the posts will be queued for rebake
- Optimized thumbnails are generated during post_process_cooked. If thumbnails are missing when serializing a topic list, then a sidekiq job is queued
- Topic lists and topics now include a `thumbnails` key, which includes all the available images:
```
"thumbnails": [
{
"max_width": null,
"max_height": null,
"url": "//example.com/original-image.png",
"width": 1380,
"height": 1840
},
{
"max_width": 1024,
"max_height": 1024,
"url": "//example.com/optimized-image.png",
"width": 768,
"height": 1024
}
]
```
- Themes can request additional thumbnail sizes by using a modifier in their `about.json` file:
```
"modifiers": {
"topic_thumbnail_sizes": [
[200, 200],
[800, 800]
],
...
```
Remember that these are generated asynchronously, so your theme should include logic to fallback to other available thumbnails if your requested size has not yet been generated
- Two new raw plugin outlets are introduced, to improve the customisability of the topic list. `topic-list-before-columns` and `topic-list-before-link`
2020-05-05 16:07:50 +08:00
|
|
|
# image_upload_id :bigint
|
2022-09-26 07:14:24 +08:00
|
|
|
# outbound_message_id :string
|
2013-05-24 10:48:32 +08:00
|
|
|
#
|
|
|
|
# Indexes
|
|
|
|
#
|
2023-02-22 02:16:43 +08:00
|
|
|
# idx_posts_created_at_topic_id (created_at,topic_id) WHERE (deleted_at IS NULL)
|
|
|
|
# idx_posts_deleted_posts (topic_id,post_number) WHERE (deleted_at IS NOT NULL)
|
|
|
|
# idx_posts_user_id_deleted_at (user_id) WHERE (deleted_at IS NULL)
|
|
|
|
# index_for_rebake_old (id) WHERE (((baked_version IS NULL) OR (baked_version < 2)) AND (deleted_at IS NULL))
|
|
|
|
# index_posts_on_id_and_baked_version (id DESC,baked_version) WHERE (deleted_at IS NULL)
|
|
|
|
# index_posts_on_id_topic_id_where_not_deleted_or_empty (id,topic_id) WHERE ((deleted_at IS NULL) AND (raw <> ''::text))
|
|
|
|
# index_posts_on_image_upload_id (image_upload_id)
|
2023-07-27 10:55:10 +08:00
|
|
|
# index_posts_on_topic_id_and_created_at (topic_id,created_at)
|
2023-02-22 02:16:43 +08:00
|
|
|
# index_posts_on_topic_id_and_percent_rank (topic_id,percent_rank)
|
|
|
|
# index_posts_on_topic_id_and_post_number (topic_id,post_number) UNIQUE
|
2024-03-27 09:56:29 +08:00
|
|
|
# index_posts_on_topic_id_and_reply_to_post_number (topic_id,reply_to_post_number)
|
2023-02-22 02:16:43 +08:00
|
|
|
# index_posts_on_topic_id_and_sort_order (topic_id,sort_order)
|
|
|
|
# index_posts_on_user_id_and_created_at (user_id,created_at)
|
|
|
|
# index_posts_user_and_likes (user_id,like_count DESC,created_at DESC) WHERE (post_number > 1)
|
2013-05-24 10:48:32 +08:00
|
|
|
#
|