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
2022-01-11 10:57:21 +08:00
self . ignored_columns = [
" avg_time " , # TODO(2021-01-04): remove
" image_url " # TODO(2021-06-01): remove
]
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
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 "
2019-10-02 12:01:53 +08:00
validates_with PostValidator , unless : :skip_validation
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
2013-02-06 03:16:51 +08:00
2019-11-25 20:32:19 +08:00
MISSING_UPLOADS || = " missing uploads "
MISSING_UPLOADS_IGNORED || = " missing uploads ignored "
2020-11-11 20:49:53 +08:00
NOTICE || = " notice "
2017-11-16 22:45: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 , - > ( user ) do
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
)
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 ) ) }
2019-04-06 07:55:24 +08:00
2016-05-21 21:17:54 +08:00
scope :for_mailing_list , - > ( user , since ) {
2017-01-14 02:46:33 +08:00
q = created_since ( since )
2019-04-06 07:55:24 +08:00
. joins ( " 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' )
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
2016-05-21 21:17:54 +08:00
}
2019-04-06 07:55:24 +08:00
2017-10-04 08:47:53 +08:00
scope :raw_match , - > ( pattern , type = 'string' ) {
type = type & . downcase
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
}
2014-03-07 17:44:04 +08:00
2019-04-10 16:22:35 +08:00
scope :have_uploads , - > {
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}/%"
)
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
if opts [ :user_ids ] != [ ] && opts [ :group_ids ] != [ ]
MessageBus . publish ( channel , message , opts )
end
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
if topic && topic . category_id && topic . category
2013-10-17 14:44:56 +08:00
topic . category . update_latest
end
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 || = [ '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? } . each do | attr |
2013-05-31 02:34:44 +08:00
define_method ( attr ) do
2019-05-07 09:27:05 +08:00
post_analyzer . public_send ( attr )
2013-05-31 02:34:44 +08:00
end
end
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?
2017-10-18 02:37:51 +08:00
2022-09-29 07:24:33 +08:00
if self . with_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
)
)
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 } \n raw: #{ 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 } \n raw: #{ 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
hosts . reject! do | h |
2020-07-27 08:23:54 +08:00
allowlisted . any? do | w |
2014-02-27 12:43:45 +08:00
h . end_with? ( w )
end
end
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
return false if acting_user . present? && ( acting_user . staged? || acting_user . mature_staged? || acting_user . has_trust_level? ( TrustLevel [ 1 ] ) )
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 )
2018-06-21 13:26:26 +08:00
. where ( [
" id = ANY(
(
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 ( / \ [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 ( / \ <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?
2017-10-18 01:31:45 +08:00
post_actions . where ( post_action_type_id : PostActionType . flag_types_without_custom . values , deleted_at : nil ) . count != 0
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
2022-09-29 07:24:33 +08:00
def with_secure_uploads?
return false if ! SiteSetting . secure_uploads?
2020-01-16 11:50:27 +08:00
SiteSetting . login_required? || \
( topic . present? && ( topic . private_message? || topic . category & . read_restricted ) )
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 || = hidden_at ?
Post . hidden_reasons [ :flag_threshold_reached_again ] :
Post . hidden_reasons [ :flag_threshold_reached ]
hiding_again = hidden_at . present?
self . hidden = true
self . hidden_at = Time . zone . now
self . hidden_reason_id = reason
2021-01-12 03:56:08 +08:00
self . skip_unique_check = true
2019-01-04 01:03:01 +08:00
2022-02-11 09:00:58 +08:00
Post . transaction do
save!
Topic . where (
" id = :topic_id AND NOT EXISTS(SELECT 1 FROM POSTS WHERE topic_id = :topic_id AND NOT hidden) " ,
topic_id : topic_id
) . update_all ( visible : false )
UserStatCountUpdater . decrement! ( self )
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 (
" flag_reasons. #{ PostActionType . types [ post_action_type_id ] } " ,
locale : SiteSetting . default_locale ,
base_path : Discourse . base_path
)
}
2021-03-11 19:21:24 +08:00
message = custom_message
if message . nil?
message = hiding_again ? :post_hidden_again : :post_hidden
end
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 )
self . topic . update ( visible : true ) if is_first_post?
UserStatCountUpdater . increment! ( self )
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
2016-01-13 01:38:49 +08:00
def full_url
" #{ Discourse . base_url } #{ url } "
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 = " "
if topic_view . page > 1
page = " ?page= #{ topic_view . page } "
end
" #{ 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
" #{ result } #{ topic_id } / #{ post_number } "
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' ] ) .
2013-04-22 15:45:03 +08:00
each do | t |
2013-04-24 16:05:35 +08:00
urls [ t . post_id . to_i ] = url ( t . slug , t . id , t . post_number )
2013-04-22 15:45:03 +08:00
end
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 |
2014-05-28 10:30:43 +08:00
begin
2019-01-04 06:24:46 +08:00
break if ! limiter . can_perform?
2018-01-05 06:53:46 +08:00
post = Post . find ( id )
2019-01-09 05:57:20 +08:00
post . rebake! ( priority : priority )
2019-01-04 06:24:46 +08:00
begin
2019-01-17 11:53:09 +08:00
limiter . performed! if rate_limiter
2019-01-04 06:24:46 +08:00
rescue RateLimiter :: LimitExceeded
break
end
2014-05-28 10:30:43 +08:00
rescue = > e
2018-01-05 06:53:46 +08:00
problems << { post : post , ex : e }
2017-12-27 09:44:41 +08:00
2018-01-05 06:53:46 +08:00
attempts = post . custom_fields [ " rebake_attempts " ] . to_i
2017-12-27 10:51:16 +08:00
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 ( e , message : " Can not rebake post # #{ post . id } after 3 attempts, giving up " )
2017-12-27 09:44:41 +08:00
else
2018-01-05 06:53:46 +08:00
post . custom_fields [ " rebake_attempts " ] = attempts + 1
post . save_custom_fields
2017-12-27 09:44:41 +08:00
end
2014-05-28 10:30:43 +08:00
end
end
2014-07-18 04:22:46 +08:00
problems
2014-05-28 10:30:43 +08:00
end
2019-01-09 05:57:20 +08:00
def rebake! ( invalidate_broken_images : false , invalidate_oneboxes : false , priority : nil )
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 16:29:00 +08:00
update_columns (
2019-04-01 10:14:29 +08:00
cooked : new_cooked ,
baked_at : Time . zone . now ,
baked_version : BAKED_VERSION
)
2014-05-28 10:30:43 +08:00
2020-05-23 12:56:13 +08:00
if is_first_post?
2020-06-01 13:04:16 +08:00
topic & . update_excerpt ( excerpt_for_topic )
2020-05-23 12:56:13 +08:00
end
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
if post_number == topic . highest_post_number
topic . update_columns ( last_post_user_id : new_user . id )
end
2014-03-28 09:28:14 +08:00
end
2013-02-06 03:16:51 +08:00
before_create do
2013-06-10 00:48:44 +08:00
PostCreator . before_create_tasks ( self )
2013-02-06 03:16:51 +08:00
end
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 ]
end
2013-02-06 03:16:51 +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 ,
2020-06-24 17:54:54 +08:00
post_id : 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 , include_subcategories = false )
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
result = result . where ( 'topics.category_id = ?' , category_id )
end
end
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
2015-09-25 08:15:58 +08:00
def reply_history ( max_replies = 100 , guardian = nil )
2018-06-19 14:13:14 +08:00
post_ids = DB . query_single ( << ~ SQL , post_id : id , topic_id : topic_id )
WITH RECURSIVE breadcrumb ( id , reply_to_post_number ) AS (
SELECT p . id , p . reply_to_post_number FROM posts AS p
WHERE p . id = :post_id
UNION
SELECT p . id , p . reply_to_post_number FROM posts AS p , breadcrumb
WHERE breadcrumb . reply_to_post_number = p . post_number
AND p . topic_id = :topic_id
)
SELECT id from breadcrumb
WHERE id < > :post_id
ORDER by id
SQL
2014-10-27 06:44:42 +08:00
# [1,2,3][-10,-1] => nil
post_ids = ( post_ids [ ( 0 - max_replies ) .. - 1 ] || post_ids )
2015-09-25 08:15:58 +08:00
Post . secured ( guardian ) . where ( id : post_ids ) . includes ( :user , :topic ) . order ( :id ) . to_a
2013-08-07 05:42:36 +08:00
end
2017-12-15 07:23:51 +08:00
MAX_REPLY_LEVEL || = 1000
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 )
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?
2020-02-14 09:17:09 +08:00
Upload . where (
id : upload_ids , access_control_post_id : nil
2020-03-02 13:40:29 +08:00
) . where (
'id NOT IN (SELECT upload_id FROM custom_emojis)'
2020-02-14 09:17:09 +08:00
) . update_all (
2020-01-16 11:50:27 +08:00
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 : )
2019-11-18 09:25:42 +08:00
if Discourse . store . external?
2021-06-22 00:15:24 +08:00
Jobs . enqueue ( :update_post_uploads_secure_status , post_id : self . id , 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
2019-05-04 03:46:20 +08:00
upload_patterns = [
2019-06-25 03:49:58 +08:00
/ \/ uploads \/ #{ current_db } \/ / ,
2019-05-04 03:46:20 +08:00
/ \/ original \/ / ,
2019-05-29 09:00:25 +08:00
/ \/ optimized \/ / ,
2019-06-19 09:10:50 +08:00
/ \/ 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 )
2020-03-10 21:01:40 +08:00
selectors = fragments . css ( " a/@href " , " img/@src " , " source/@src " , " track/@src " , " video/@poster " )
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?
if src . end_with? ( " /images/transparent.png " ) && ( parent = media . parent ) [ " data-orig-src " ] . present?
parent [ " data-orig-src " ]
else
src
end
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
2019-09-22 18:02:28 +08:00
elsif src . include? ( " /uploads/short-url/ " )
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? ( " // " )
2022-09-29 07:24:33 +08:00
next unless Discourse . store . has_been_uploaded? ( src ) || Upload . secure_uploads_url? ( src ) || ( include_local_upload && src =~ / \ A \/ [^ \/ ] /i )
2019-05-04 03:46:20 +08:00
path = begin
2019-12-12 10:49:21 +08:00
URI ( UrlHelper . unencode ( GlobalSetting . cdn_url ? src . sub ( GlobalSetting . cdn_url , " " ) : src ) ) & . path
2019-05-04 03:46:20 +08:00
rescue URI :: Error
end
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
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
2019-10-21 18:32:27 +08:00
upload_id = Upload . where ( sha1 : sha1 ) . pluck_first ( :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 )
return { } unless quote . present?
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 do | arg |
2014-08-18 11:00:02 +08:00
args [ arg [ 0 ] ] = arg [ 1 ] . to_i
2013-05-23 03:38:45 +08:00
end
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 )
return unless num . present?
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
#
2018-07-16 14:18:07 +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)
2019-04-11 12:37:11 +08:00
# index_for_rebake_old (id) WHERE (((baked_version IS NULL) OR (baked_version < 2)) AND (deleted_at IS NULL))
2019-04-09 13:27:22 +08:00
# index_posts_on_id_and_baked_version (id DESC,baked_version) WHERE (deleted_at IS NULL)
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
# index_posts_on_image_upload_id (image_upload_id)
2018-07-16 14:18:07 +08:00
# index_posts_on_reply_to_post_number (reply_to_post_number)
# 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
# index_posts_on_topic_id_and_sort_order (topic_id,sort_order)
# index_posts_on_user_id_and_created_at (user_id,created_at)
2020-12-29 12:54:05 +08:00
# 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
#