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
2020-05-14 04:08:15 +08:00
# TODO(2021-01-04): remove
2020-04-30 09:22:20 +08:00
self . ignored_columns = [ " avg_time " ]
2018-07-25 23:44:09 +08:00
cattr_accessor :plugin_permitted_create_params
self . plugin_permitted_create_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
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
has_many :post_actions
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
2013-06-14 05:44:24 +08:00
has_many :post_uploads
has_many :uploads , through : :post_uploads
2013-06-13 07:43:50 +08:00
2015-08-03 12:29:04 +08:00
has_one :post_stat
2020-02-13 14:26:02 +08:00
has_many :bookmarks
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 "
2019-10-02 12:01:53 +08:00
validates_with PostValidator , unless : :skip_validation
2013-02-06 03:16:51 +08:00
2016-12-22 10:13:14 +08:00
after_save :index_search
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
LARGE_IMAGES || = " large_images "
BROKEN_IMAGES || = " broken_images "
DOWNLOADED_IMAGES || = " downloaded_images "
MISSING_UPLOADS || = " missing uploads "
MISSING_UPLOADS_IGNORED || = " missing uploads ignored "
NOTICE_TYPE || = " notice_type "
NOTICE_ARGS || = " notice_args "
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
2017-05-12 03:58:43 +08:00
scope :private_posts_for_user , - > ( user ) {
2020-03-23 19:02:24 +08:00
where ( " posts.topic_id IN ( #{ Topic :: PRIVATE_MESSAGES_SQL } ) " , user_id : user . id )
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
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
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 )
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 )
2018-03-05 15:38:05 +08:00
if topic . private_message?
2018-12-06 08:20:36 +08:00
opts [ :user_ids ] = User . human_users . where ( " admin OR moderator " ) . pluck ( :id )
2018-12-06 04:27:49 +08:00
opts [ :user_ids ] |= topic . allowed_users . pluck ( :id )
2017-09-09 05:09:05 +08:00
else
2018-12-06 04:27:49 +08:00
opts [ :group_ids ] = topic . secure_group_ids
2017-09-09 05:09:05 +08:00
end
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
MessageBus . publish ( channel , message , opts )
2014-08-29 11:34:32 +08:00
end
2017-07-28 09:20:09 +08:00
def trash! ( trashed_by = nil )
2013-06-14 01:41:45 +08:00
self . topic_links . each ( & :destroy )
2019-03-11 17:19:58 +08:00
self . delete_post_notices
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
2015-02-03 01:44:21 +08:00
" unique-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
2013-02-12 15:43:48 +08:00
def self . white_listed_image_classes
2017-09-15 13:02:11 +08:00
@white_listed_image_classes || = [ 'avatar' , 'favicon' , 'thumbnail' , 'emoji' ]
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
image_count
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
post_user = self . user
options [ :user_id ] = post_user . id if post_user
2018-09-17 10:02:20 +08:00
options [ :omit_nofollow ] = true if omit_nofollow?
2017-10-18 02:37:51 +08:00
2019-11-18 09:25:42 +08:00
if self . with_secure_media?
each_upload_url do | url |
uri = URI . parse ( url )
if FileHelper . is_supported_media? ( File . basename ( uri . path ) )
2020-01-24 09:59:30 +08:00
raw = raw . sub ( Discourse . store . s3_upload_host , " #{ Discourse . base_url } / #{ Upload :: SECURE_MEDIA_ROUTE } " )
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
2014-02-27 12:43:45 +08:00
def whitelisted_spam_hosts
hosts = SiteSetting
2017-07-28 09:20:09 +08:00
. white_listed_spam_host_domains
. split ( '|' )
. map { | h | h . strip }
. 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
2014-02-27 12:43:45 +08:00
whitelisted = whitelisted_spam_hosts
hosts . reject! do | h |
whitelisted . any? do | w |
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 )
2017-07-28 09:20:09 +08:00
. group ( :domain , :post_id )
. 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
2019-11-25 20:32:19 +08:00
self . custom_fields . delete ( Post :: NOTICE_TYPE )
self . custom_fields . delete ( Post :: NOTICE_ARGS )
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
2020-05-23 12:56:13 +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
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
2019-11-18 09:25:42 +08:00
def with_secure_media?
2020-01-16 11:50:27 +08:00
return false if ! SiteSetting . secure_media?
SiteSetting . login_required? || \
( topic . present? && ( topic . private_message? || topic . category & . read_restricted ) )
2019-11-18 09:25:42 +08:00
end
2019-01-04 01:03:01 +08:00
def hide! ( post_action_type_id , reason = nil )
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
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 )
# 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
)
}
Jobs . enqueue_in (
5 . seconds ,
:send_system_message ,
user_id : user . id ,
message_type : hiding_again ? :post_hidden_again : :post_hidden ,
message_options : options
)
end
2016-03-31 01:27:34 +08:00
end
2013-02-07 12:15:48 +08:00
def unhide!
2019-04-29 15:32:25 +08:00
self . update ( hidden : false )
self . topic . update ( visible : true ) if is_first_post?
2014-08-11 16:48:00 +08:00
save ( validate : false )
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-07-28 09:20:09 +08:00
def url ( opts = nil )
2017-04-25 03:26:06 +08:00
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
2016-06-17 09:27:52 +08:00
def unsubscribe_url ( user )
" #{ Discourse . base_url } /email/unsubscribe/ #{ UnsubscribeKey . create_key_for ( user , self ) } "
end
2017-07-28 09:20:09 +08:00
def self . url ( slug , topic_id , post_number , opts = nil )
2017-04-25 03:26:06 +08:00
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 )
2017-07-28 09:20:09 +08:00
ids = post_ids . map { | u | u }
2013-04-22 15:45:03 +08:00
if ids . length > 0
urls = { }
Topic . joins ( :posts ) . where ( 'posts.id' = > ids ) .
2017-07-28 09:20:09 +08:00
select ( [ 'posts.id as post_id' , 'post_number' , 'topics.slug' , 'topics.title' , 'topics.id' ] ) .
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
2017-07-28 09:20:09 +08:00
def revise ( updated_by , changes = { } , opts = { } )
2014-10-28 05:06:43 +08:00
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?
topic . update_excerpt ( excerpt_for_topic )
end
2019-01-09 05:57:20 +08:00
if invalidate_broken_images
2018-12-27 01:52:07 +08:00
custom_fields . delete ( BROKEN_IMAGES )
save_custom_fields
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
2017-07-28 09:20:09 +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
2017-08-31 12:06:56 +08:00
if ! new_record? && will_save_change_to_raw?
2016-10-24 12:02:38 +08:00
self . cooked = cook ( raw , topic_id : topic_id )
end
2020-03-10 00:37:49 +08:00
self . baked_at = Time . zone . now
2014-05-30 12:45:39 +08:00
self . baked_version = BAKED_VERSION
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
2019-01-17 10:24:32 +08:00
def trigger_post_process ( bypass_bump : false , priority : :normal , new_post : false )
2013-11-22 08:52:26 +08:00
args = {
post_id : id ,
2018-09-06 09:58:01 +08:00
bypass_bump : bypass_bump ,
2019-01-17 10:24:32 +08:00
new_post : new_post ,
2013-11-22 08:52:26 +08:00
}
2013-03-01 02:54:12 +08:00
args [ :image_sizes ] = image_sizes if image_sizes . present?
args [ :invalidate_oneboxes ] = true if invalidate_oneboxes . present?
2015-09-30 00:51:26 +08:00
args [ :cooking_options ] = self . cooking_options
2019-01-09 05:57:20 +08:00
2019-01-17 11:53:09 +08:00
if priority && priority != :normal
args [ :queue ] = priority . to_s
2019-01-09 05:57:20 +08:00
end
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
2017-07-28 09:20:09 +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
2017-07-28 09:20:09 +08:00
post_ids = ( post_ids [ ( 0 - max_replies ) .. - 1 ] || post_ids )
2014-10-27 06:44:42 +08:00
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
SearchIndexer . index ( self )
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
upload_ids |= Upload . where ( id : downloaded_images . values ) . pluck ( :id )
2020-01-16 11:50:27 +08:00
post_uploads = upload_ids . map do | upload_id |
{ post_id : self . id , upload_id : upload_id }
2019-11-18 09:25:42 +08:00
end
2018-09-06 09:58:01 +08:00
PostUpload . transaction do
PostUpload . where ( post_id : self . id ) . delete_all
2020-01-16 11:50:27 +08:00
if post_uploads . size > 0
PostUpload . insert_all ( post_uploads )
end
if SiteSetting . secure_media?
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
2019-11-18 09:25:42 +08:00
def update_uploads_secure_status
if Discourse . store . external?
self . uploads . each { | upload | upload . update_secure_status }
end
end
2018-09-06 09:58:01 +08:00
def downloaded_images
JSON . parse ( self . custom_fields [ Post :: DOWNLOADED_IMAGES ] . presence || " {} " )
rescue JSON :: ParserError
{ }
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? ( " // " )
2019-07-29 20:02:18 +08:00
next unless Discourse . store . has_been_uploaded? ( 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 )
sha1s = Upload . joins ( :post_uploads ) . where ( " post_uploads.post_id >= ? AND post_uploads.post_id <= ? " , ids . min , ids . max ) . pluck ( :sha1 )
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
image_upload & . url
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
2016-10-31 17:41:33 +08:00
# image_url :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
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)
2013-05-24 10:48:32 +08:00
#