2019-05-02 18:17:27 -04:00
# frozen_string_literal: true
2013-02-05 14:16:51 -05:00
require 'archetype'
require 'digest/sha1'
class Post < ActiveRecord :: Base
include RateLimiter :: OnCreateRecord
2013-05-07 00:39:01 -04:00
include Trashable
2017-08-15 11:46:57 -04:00
include Searchable
2014-04-28 04:31:51 -04:00
include HasCustomFields
2015-02-25 14:53:21 -05:00
include LimitedEdit
2013-02-05 14:16:51 -05:00
2020-05-13 16:08:15 -04:00
# TODO(2021-01-04): remove
2020-04-29 21:22:20 -04:00
self . ignored_columns = [ " avg_time " ]
2018-07-25 11:44:09 -04:00
cattr_accessor :plugin_permitted_create_params
self . plugin_permitted_create_params = { }
2017-08-11 22:10:45 -04:00
2014-05-30 00:45:39 -04:00
# increase this number to force a system wide post rebake
2019-04-09 01:54:14 -04:00
# Recreate `index_for_rebake_old` when the number is increased
2017-12-14 18:28:07 -05: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-27 22:30:43 -04:00
2013-02-07 10:45:24 -05:00
rate_limit
2013-10-09 19:32:03 -04:00
rate_limit :limit_posts_per_day
2013-02-19 01:57:14 -05:00
2013-02-05 14:16:51 -05:00
belongs_to :user
2016-12-02 01:03:31 -05:00
belongs_to :topic
2014-07-16 15:04:55 -04:00
2013-03-19 19:51:39 -04:00
belongs_to :reply_to_user , class_name : " User "
2013-02-05 14:16:51 -05:00
has_many :post_replies
has_many :replies , through : :post_replies
has_many :post_actions
2013-06-13 13:41:45 -04:00
has_many :topic_links
2015-12-01 00:52:43 -05:00
has_many :group_mentions , dependent : :destroy
2013-02-05 14:16:51 -05:00
2013-06-13 17:44:24 -04:00
has_many :post_uploads
has_many :uploads , through : :post_uploads
2013-06-12 19:43:50 -04:00
2015-08-03 00:29:04 -04:00
has_one :post_stat
2020-02-13 01:26:02 -05:00
has_many :bookmarks
2013-05-22 15:33:33 -04:00
2016-04-20 15:29:27 -04:00
has_one :incoming_email
2013-10-15 10:21:30 -04:00
has_many :post_details
2013-12-11 21:41:34 -05:00
has_many :post_revisions
2018-05-28 19:34:12 -04:00
has_many :revisions , - > { order ( :number ) } , foreign_key : :post_id , class_name : 'PostRevision'
2013-12-11 21:41:34 -05:00
2014-07-16 15:04:55 -04: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 04:07:50 -04:00
belongs_to :image_upload , class_name : " Upload "
2019-10-02 00:01:53 -04:00
validates_with PostValidator , unless : :skip_validation
2013-02-05 14:16:51 -05:00
2016-12-21 21:13:14 -05:00
after_save :index_search
2013-06-21 11:36:33 -04:00
# We can pass several creating options to a post via attributes
2019-01-02 09:24:13 -05:00
attr_accessor :image_sizes , :quoted_post_numbers , :no_bump , :invalidate_oneboxes , :cooking_options , :skip_unique_check , :skip_validation
2013-02-05 14:16:51 -05:00
2019-11-25 07:32:19 -05: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 09:45:07 -05:00
SHORT_POST_CHARS || = 1200
2013-02-05 14:16:51 -05:00
2019-04-10 08:39:35 -04:00
register_custom_field_type ( MISSING_UPLOADS , :json )
2019-05-08 19:41:15 -04:00
register_custom_field_type ( MISSING_UPLOADS_IGNORED , :boolean )
2019-04-10 08:39:35 -04:00
2017-05-11 15:58:43 -04:00
scope :private_posts_for_user , - > ( user ) {
2020-03-23 07:02:24 -04:00
where ( " posts.topic_id IN ( #{ Topic :: PRIVATE_MESSAGES_SQL } ) " , user_id : user . id )
2017-05-11 15:58:43 -04:00
}
2017-09-14 14:08:16 -04:00
scope :by_newest , - > { order ( 'created_at DESC, id DESC' ) }
2013-06-09 12:48:44 -04:00
scope :by_post_number , - > { order ( 'post_number ASC' ) }
scope :with_user , - > { includes ( :user ) }
2017-09-14 14:08:16 -04:00
scope :created_since , - > ( time_ago ) { where ( 'posts.created_at > ?' , time_ago ) }
2013-04-10 08:54:10 -04: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-16 16:56:18 -04:00
scope :with_topic_subtype , - > ( subtype ) { joins ( :topic ) . where ( 'topics.subtype = ?' , subtype ) }
2014-06-26 13:48:07 -04:00
scope :visible , - > { joins ( :topic ) . where ( 'topics.visible = true' ) . where ( hidden : false ) }
2017-09-14 14:08:16 -04:00
scope :secured , - > ( guardian ) { where ( 'posts.post_type IN (?)' , Topic . visible_post_types ( guardian & . user ) ) }
2019-04-05 19:55:24 -04:00
2016-05-21 09:17:54 -04:00
scope :for_mailing_list , - > ( user , since ) {
2017-01-13 13:46:33 -05:00
q = created_since ( since )
2019-04-05 19:55:24 -04: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-13 13:46:33 -05:00
q = q . where . not ( post_type : Post . types [ :whisper ] ) unless user . staff?
2019-04-05 19:55:24 -04:00
q
2016-05-21 09:17:54 -04:00
}
2019-04-05 19:55:24 -04:00
2017-10-03 20:47:53 -04:00
scope :raw_match , - > ( pattern , type = 'string' ) {
type = type & . downcase
case type
when 'string'
where ( 'raw ILIKE ?' , " % #{ pattern } % " )
when 'regex'
2018-08-23 08:49:00 -04:00
where ( 'raw ~* ?' , " (?n) #{ pattern } " )
2017-10-03 20:47:53 -04:00
end
}
2014-03-07 04:44:04 -05:00
2019-04-10 04:22:35 -04:00
scope :have_uploads , - > {
2019-09-24 13:47:59 -04: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 04:22:35 -04:00
}
2014-01-14 11:15:35 -05:00
delegate :username , to : :user
2014-03-07 04:44:04 -05:00
2013-03-18 14:59:34 -04:00
def self . hidden_reasons
2016-01-08 05:53:52 -05:00
@hidden_reasons || = Enum . new ( flag_threshold_reached : 1 ,
flag_threshold_reached_again : 2 ,
new_user_spam_threshold_reached : 3 ,
2018-07-05 05:07:46 -04:00
flagged_by_tl3_user : 4 ,
2018-10-10 11:50:00 -04:00
email_spam_header_found : 5 ,
2019-11-26 09:55:22 -05:00
flagged_by_tl4_user : 6 ,
2020-04-13 15:17:02 -04:00
email_authentication_result_header : 7 ,
imported_as_unlisted : 8 )
2013-03-18 14:59:34 -04:00
end
2013-03-18 16:03:46 -04:00
def self . types
2016-01-08 05:53:52 -05:00
@types || = Enum . new ( regular : 1 ,
moderator_action : 2 ,
small_action : 3 ,
whisper : 4 )
2013-03-18 16:03:46 -04:00
end
2013-12-31 14:37:43 -05:00
def self . cook_methods
2016-01-08 05:53:52 -05:00
@cook_methods || = Enum . new ( regular : 1 ,
raw_html : 2 ,
email : 3 )
2013-12-31 14:37:43 -05:00
end
2019-04-19 10:53:58 -04:00
def self . notices
@notices || = Enum . new ( custom : " custom " ,
new_user : " new_user " ,
returning_user : " returning_user " )
end
2013-10-15 10:21:30 -04:00
def self . find_by_detail ( key , value )
2014-05-06 09:41:59 -04:00
includes ( :post_details ) . find_by ( post_details : { key : key , value : value } )
2013-10-15 10:21:30 -04:00
end
2019-10-21 13:44:14 -04:00
def self . excerpt_size = ( sz )
@excerpt_size = sz
end
2019-10-16 16:28:12 -04:00
def self . excerpt_size
2019-10-21 13:44:14 -04:00
@excerpt_size || 220
2019-10-16 16:28:12 -04:00
end
2016-01-11 11:47:17 -05:00
def whisper?
post_type == Post . types [ :whisper ]
end
2013-10-15 10:21:30 -04:00
def add_detail ( key , value , extra = nil )
post_details . build ( key : key , value : value , extra : extra )
end
2013-10-09 19:32:03 -04:00
def limit_posts_per_day
2016-06-20 16:38:15 -04:00
if user && user . new_user_posting_on_first_day? && post_number && post_number > 1
2015-02-11 01:45:46 -05:00
RateLimiter . new ( user , " first-day-replies-per-day " , SiteSetting . max_replies_in_first_day , 1 . day . to_i )
2013-10-09 19:32:03 -04:00
end
end
2019-09-08 21:29:15 -04:00
def readers_count
read_count = reads - 1 # Excludes poster
read_count < 0 ? 0 : read_count
end
2018-12-05 15:27:49 -05: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-10 16:01:23 -04:00
return unless topic
2018-12-05 15:27:49 -05:00
message = {
2015-09-21 18:50:52 -04:00
id : id ,
post_number : post_number ,
updated_at : Time . now ,
2015-10-11 21:45:04 -04:00
user_id : user_id ,
last_editor_id : last_editor_id ,
2017-01-20 01:37:22 -05:00
type : type ,
version : version
2018-12-05 15:27:49 -05:00
} . merge ( opts )
publish_message! ( " /topic/ #{ topic_id } " , message )
end
def publish_message! ( channel , message , opts = { } )
return unless topic
2015-09-21 18:50:52 -04:00
2015-09-24 20:15:58 -04:00
if Topic . visible_post_types . include? ( post_type )
2018-03-05 02:38:05 -05:00
if topic . private_message?
2018-12-05 19:20:36 -05:00
opts [ :user_ids ] = User . human_users . where ( " admin OR moderator " ) . pluck ( :id )
2018-12-05 15:27:49 -05:00
opts [ :user_ids ] |= topic . allowed_users . pluck ( :id )
2017-09-08 17:09:05 -04:00
else
2018-12-05 15:27:49 -05:00
opts [ :group_ids ] = topic . secure_group_ids
2017-09-08 17:09:05 -04:00
end
2015-09-24 20:15:58 -04:00
else
2018-12-05 19:20:36 -05:00
opts [ :user_ids ] = User . human_users
. where ( " admin OR moderator OR id = ? " , user_id )
. pluck ( :id )
2015-09-10 16:01:23 -04:00
end
2018-12-05 15:27:49 -05:00
MessageBus . publish ( channel , message , opts )
2014-08-28 23:34:32 -04:00
end
2017-07-27 21:20:09 -04:00
def trash! ( trashed_by = nil )
2013-06-13 13:41:45 -04:00
self . topic_links . each ( & :destroy )
2019-03-11 05:19:58 -04:00
self . delete_post_notices
2013-07-09 15:20:18 -04:00
super ( trashed_by )
2013-06-13 13:41:45 -04:00
end
2013-05-07 00:39:01 -04:00
def recover!
super
2018-10-02 11:25:08 -04:00
recover_public_post_actions
2013-06-13 13:41:45 -04:00
TopicLink . extract_from ( self )
2014-07-15 03:47:24 -04:00
QuotedPost . extract_from ( self )
2013-10-23 19:05:51 -04:00
if topic && topic . category_id && topic . category
2013-10-17 02:44:56 -04:00
topic . category . update_latest
end
2013-05-07 00:39:01 -04:00
end
2013-03-22 06:18:48 -04:00
# The key we use in redis to ensure unique posts
2013-02-05 14:16:51 -05:00
def unique_post_key
2015-02-02 12:44:21 -05:00
" unique-post- #{ user_id } : #{ raw_hash } "
2013-02-05 14:16:51 -05:00
end
2013-09-09 16:17:31 -04:00
def store_unique_post_key
if SiteSetting . unique_posts_mins > 0
2019-12-03 04:05:53 -05:00
Discourse . redis . setex ( unique_post_key , SiteSetting . unique_posts_mins . minutes . to_i , id )
2013-09-09 16:17:31 -04:00
end
end
def matches_recent_post?
2019-12-03 04:05:53 -05:00
post_id = Discourse . redis . get ( unique_post_key )
2019-03-13 20:15:09 -04:00
post_id != ( nil ) && post_id . to_i != ( id )
2013-09-09 16:17:31 -04:00
end
2013-02-05 14:16:51 -05:00
def raw_hash
2013-02-28 13:54:12 -05:00
return if raw . blank?
2014-06-15 22:14:06 -04:00
Digest :: SHA1 . hexdigest ( raw )
2013-02-05 14:16:51 -05:00
end
2013-02-12 02:43:48 -05:00
def self . white_listed_image_classes
2017-09-15 01:02:11 -04:00
@white_listed_image_classes || = [ 'avatar' , 'favicon' , 'thumbnail' , 'emoji' ]
2013-02-12 02:43:48 -05:00
end
2013-05-30 14:34:44 -04:00
def post_analyzer
2013-07-22 16:24:47 -04:00
@post_analyzers || = { }
@post_analyzers [ raw_hash ] || = PostAnalyzer . new ( raw , topic_id )
2013-05-30 14:34:44 -04:00
end
2013-02-12 02:43:48 -05:00
2018-02-08 18:26:56 -05:00
%w{ raw_mentions
linked_hosts
image_count
attachment_count
link_count
raw_links
has_oneboxes? } . each do | attr |
2013-05-30 14:34:44 -04:00
define_method ( attr ) do
2019-05-06 21:27:05 -04:00
post_analyzer . public_send ( attr )
2013-05-30 14:34:44 -04:00
end
end
2016-08-12 15:28:54 -04:00
def add_nofollow?
2018-09-16 22:02:20 -04:00
return false if user & . staff?
2016-08-15 12:57:58 -04:00
user . blank? || SiteSetting . tl3_links_no_follow? || ! user . has_trust_level? ( TrustLevel [ 3 ] )
2016-08-12 15:28:54 -04:00
end
def omit_nofollow?
2016-08-15 12:57:58 -04:00
! add_nofollow?
2016-08-12 15:28:54 -04:00
end
2017-10-17 14:37:51 -04:00
def cook ( raw , opts = { } )
2013-12-31 14:37:43 -05: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-17 14:37:51 -04:00
options = opts . dup
options [ :cook_method ] = cook_method
post_user = self . user
options [ :user_id ] = post_user . id if post_user
2018-09-16 22:02:20 -04:00
options [ :omit_nofollow ] = true if omit_nofollow?
2017-10-17 14:37:51 -04:00
2019-11-17 20:25:42 -05: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-23 20:59:30 -05:00
raw = raw . sub ( Discourse . store . s3_upload_host , " #{ Discourse . base_url } / #{ Upload :: SECURE_MEDIA_ROUTE } " )
2019-11-17 20:25:42 -05:00
end
end
end
2018-09-16 22:02:20 -04:00
cooked = post_analyzer . cook ( raw , options )
2014-11-23 18:34:29 -05:00
new_cooked = Plugin :: Filter . apply ( :after_post_cook , self , cooked )
2015-07-29 14:54:33 -04:00
if post_type == Post . types [ :regular ]
if new_cooked != cooked && new_cooked . blank?
2017-10-17 14:37:51 -04:00
Rails . logger . debug ( " Plugin is blanking out post: #{ self . url } \n raw: #{ raw } " )
2015-07-29 14:54:33 -04:00
elsif new_cooked . blank?
2017-10-17 14:37:51 -04:00
Rails . logger . debug ( " Blank post detected post: #{ self . url } \n raw: #{ raw } " )
2015-07-29 14:54:33 -04:00
end
2014-11-23 18:34:29 -05:00
end
new_cooked
2013-02-05 14:16:51 -05:00
end
2013-04-05 13:59:00 -04: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-08 15:26:06 -05:00
def last_editor
self . last_editor_id ? ( User . find_by_id ( self . last_editor_id ) || user ) : user
end
2014-02-26 23:43:45 -05:00
def whitelisted_spam_hosts
hosts = SiteSetting
2017-07-27 21:20:09 -04:00
. white_listed_spam_host_domains
. split ( '|' )
. map { | h | h . strip }
. reject { | h | ! h . include? ( '.' ) }
2014-02-26 23:43:45 -05:00
hosts << GlobalSetting . hostname
2014-04-28 10:37:28 -04:00
hosts << RailsMultisite :: ConnectionManagement . current_hostname
2014-02-26 23:43:45 -05:00
end
2013-05-10 16:58:23 -04:00
def total_hosts_usage
hosts = linked_hosts . clone
2014-02-26 23:43:45 -05: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-10 16:58:23 -04:00
2013-05-24 15:20:58 -04:00
TopicLink . where ( domain : hosts . keys , user_id : acting_user . id )
2017-07-27 21:20:09 -04:00
. group ( :domain , :post_id )
. count
. each_key do | tuple |
2013-05-24 15:20:58 -04:00
domain = tuple [ 0 ]
hosts [ domain ] = ( hosts [ domain ] || 0 ) + 1
2013-05-10 16:58:23 -04:00
end
hosts
end
# Prevent new users from posting the same hosts too many times.
def has_host_spam?
2018-06-18 20:05:04 -04:00
return false if acting_user . present? && ( acting_user . staged? || acting_user . mature_staged? || acting_user . has_trust_level? ( TrustLevel [ 1 ] ) )
2017-08-10 17:18:57 -04:00
return false if topic & . private_message?
2013-05-10 16:58:23 -04:00
2016-04-25 17:03:17 -04:00
total_hosts_usage . values . any? { | count | count > = SiteSetting . newuser_spam_host_threshold }
2013-05-10 16:58:23 -04:00
end
2013-02-05 14:16:51 -05:00
def archetype
2017-09-12 13:04:53 -04:00
topic & . archetype
2013-02-05 14:16:51 -05:00
end
2013-02-07 10:45:24 -05:00
2013-02-05 14:16:51 -05:00
def self . regular_order
2013-02-07 10:45:24 -05:00
order ( :sort_order , :post_number )
2013-02-05 14:16:51 -05:00
end
def self . reverse_order
2013-02-07 10:45:24 -05:00
order ( 'sort_order desc, post_number desc' )
2013-02-05 14:16:51 -05:00
end
2018-06-21 00:00:54 -04:00
def self . summary ( topic_id )
topic_id = topic_id . to_i
2015-01-30 01:19:42 -05:00
# percent rank has tons of ties
2018-06-21 00:00:54 -04:00
where ( topic_id : topic_id )
2018-06-21 01:26:26 -04:00
. where ( [
" id = ANY(
(
SELECT posts . id
FROM posts
WHERE posts . topic_id = #{topic_id.to_i}
2018-06-21 02:00:20 -04:00
AND posts . post_number = 1
2018-06-21 01:26:26 -04: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 00:00:54 -04:00
) " ,
SiteSetting . summary_percent_filter . to_f / 100 . 0 ,
SiteSetting . summary_max_results
] )
2013-02-05 14:16:51 -05:00
end
2019-03-08 03:48:35 -05:00
def delete_post_notices
2019-11-25 07:32:19 -05:00
self . custom_fields . delete ( Post :: NOTICE_TYPE )
self . custom_fields . delete ( Post :: NOTICE_ARGS )
2019-03-11 05:19:58 -04:00
self . save_custom_fields
2019-03-08 03:48:35 -05:00
end
2018-10-02 11:25:08 -04: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-02-28 13:54:12 -05:00
def filter_quotes ( parent_post = nil )
2013-02-05 14:16:51 -05:00
return cooked if parent_post . blank?
# We only filter quotes when there is exactly 1
return cooked unless ( quote_count == 1 )
2013-02-15 20:58:33 -05:00
parent_raw = parent_post . raw . sub ( / \ [quote.+ \/ quote \ ] /m , '' )
2013-02-05 14:16:51 -05:00
2013-03-04 19:42:44 -05:00
if raw [ parent_raw ] || ( parent_raw . size < SHORT_POST_CHARS )
2013-02-05 14:16:51 -05:00
return cooked . sub ( / \ <aside.+ \ < \/ aside \ > /m , '' )
end
cooked
end
def external_id
2013-02-07 10:45:24 -05:00
" #{ topic_id } / #{ post_number } "
2013-02-05 14:16:51 -05:00
end
2014-01-03 12:52:24 -05:00
def reply_to_post
return if reply_to_post_number . blank?
2014-05-06 09:41:59 -04: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-03 12:52:24 -05:00
end
2013-02-05 14:16:51 -05:00
def reply_notification_target
2013-02-28 13:54:12 -05:00
return if reply_to_post_number . blank?
2014-05-06 09:41:59 -04: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-05 14:16:51 -05:00
end
2013-04-29 23:25:55 -04:00
def self . excerpt ( cooked , maxlength = nil , options = { } )
2013-02-05 14:16:51 -05:00
maxlength || = SiteSetting . post_excerpt_maxlength
2013-04-29 23:25:55 -04:00
PrettyText . excerpt ( cooked , maxlength , options )
2013-02-05 14:16:51 -05:00
end
# Strip out most of the markup
2013-04-29 23:25:55 -04:00
def excerpt ( maxlength = nil , options = { } )
2019-05-29 11:05:52 -04:00
Post . excerpt ( cooked , maxlength , options . merge ( post : self ) )
2013-02-05 14:16:51 -05:00
end
2018-04-17 15:08:13 -04:00
def excerpt_for_topic
2019-10-16 16:28:12 -04:00
Post . excerpt ( cooked , Post . excerpt_size , strip_links : true , strip_images : true , post : self )
2018-04-17 15:08:13 -04:00
end
2013-05-25 20:18:04 -04:00
def is_first_post?
2015-04-23 13:33:29 -04:00
post_number . blank? ?
topic . try ( :highest_post_number ) == 0 :
post_number == 1
2013-05-25 20:18:04 -04:00
end
2016-08-10 13:24:01 -04:00
def is_reply_by_email?
via_email && post_number . present? && post_number > 1
end
2013-02-07 10:45:24 -05:00
def is_flagged?
2017-10-17 13:31:45 -04:00
post_actions . where ( post_action_type_id : PostActionType . flag_types_without_custom . values , deleted_at : nil ) . count != 0
2013-02-06 23:15:48 -05:00
end
2019-01-03 12:03:01 -05:00
def reviewable_flag
ReviewableFlaggedPost . pending . find_by ( target : self )
end
2019-11-17 20:25:42 -05:00
def with_secure_media?
2020-01-15 22:50:27 -05:00
return false if ! SiteSetting . secure_media?
SiteSetting . login_required? || \
( topic . present? && ( topic . private_message? || topic . category & . read_restricted ) )
2019-11-17 20:25:42 -05:00
end
2019-01-03 12:03:01 -05: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-30 13:27:34 -04:00
end
2013-02-06 23:15:48 -05:00
def unhide!
2019-04-29 03:32:25 -04:00
self . update ( hidden : false )
self . topic . update ( visible : true ) if is_first_post?
2014-08-11 04:48:00 -04:00
save ( validate : false )
2014-09-22 12:55:13 -04:00
publish_change_to_clients! ( :acted )
2013-02-06 23:15:48 -05:00
end
2016-01-12 12:38:49 -05:00
def full_url
" #{ Discourse . base_url } #{ url } "
end
2017-07-27 21:20:09 -04:00
def url ( opts = nil )
2017-04-24 15:26:06 -04:00
opts || = { }
2015-08-11 17:28:36 -04:00
if topic
2017-04-24 15:26:06 -04:00
Post . url ( topic . slug , topic . id , post_number , opts )
2015-08-11 17:28:36 -04:00
else
" /404 "
end
2013-04-22 03:45:03 -04:00
end
2016-06-16 21:27:52 -04:00
def unsubscribe_url ( user )
" #{ Discourse . base_url } /email/unsubscribe/ #{ UnsubscribeKey . create_key_for ( user , self ) } "
end
2017-07-27 21:20:09 -04:00
def self . url ( slug , topic_id , post_number , opts = nil )
2017-04-24 15:26:06 -04:00
opts || = { }
2019-05-02 18:17:27 -04:00
result = + " /t/ "
result << " #{ slug } / " if ! opts [ :without_slug ]
2017-04-24 15:26:06 -04:00
" #{ result } #{ topic_id } / #{ post_number } "
2013-04-22 03:45:03 -04:00
end
def self . urls ( post_ids )
2017-07-27 21:20:09 -04:00
ids = post_ids . map { | u | u }
2013-04-22 03:45:03 -04:00
if ids . length > 0
urls = { }
Topic . joins ( :posts ) . where ( 'posts.id' = > ids ) .
2017-07-27 21:20:09 -04:00
select ( [ 'posts.id as post_id' , 'post_number' , 'topics.slug' , 'topics.title' , 'topics.id' ] ) .
each do | t |
2013-04-24 04:05:35 -04:00
urls [ t . post_id . to_i ] = url ( t . slug , t . id , t . post_number )
2013-04-22 03:45:03 -04:00
end
urls
2013-04-24 04:05:35 -04:00
else
2013-04-22 03:45:03 -04:00
{ }
end
2013-02-05 14:16:51 -05:00
end
2017-07-27 21:20:09 -04:00
def revise ( updated_by , changes = { } , opts = { } )
2014-10-27 17:06:43 -04:00
PostRevisor . new ( self ) . revise! ( updated_by , changes , opts )
2013-02-09 10:33:07 -05:00
end
2019-01-16 22:53:09 -05:00
def self . rebake_old ( limit , priority : :normal , rate_limiter : true )
2019-01-03 17:24:46 -05:00
limiter = RateLimiter . new (
nil ,
" global_periodical_rebake_limit " ,
GlobalSetting . max_old_rebakes_per_15_minutes ,
900 ,
global : true
)
2014-07-17 16:22:46 -04:00
problems = [ ]
2014-05-30 00:45:39 -04:00
Post . where ( 'baked_version IS NULL OR baked_version < ?' , BAKED_VERSION )
2017-12-14 18:28:07 -05:00
. order ( 'id desc' )
2018-01-04 17:53:46 -05:00
. limit ( limit ) . pluck ( :id ) . each do | id |
2014-05-27 22:30:43 -04:00
begin
2019-01-03 17:24:46 -05:00
break if ! limiter . can_perform?
2018-01-04 17:53:46 -05:00
post = Post . find ( id )
2019-01-08 16:57:20 -05:00
post . rebake! ( priority : priority )
2019-01-03 17:24:46 -05:00
begin
2019-01-16 22:53:09 -05:00
limiter . performed! if rate_limiter
2019-01-03 17:24:46 -05:00
rescue RateLimiter :: LimitExceeded
break
end
2014-05-27 22:30:43 -04:00
rescue = > e
2018-01-04 17:53:46 -05:00
problems << { post : post , ex : e }
2017-12-26 20:44:41 -05:00
2018-01-04 17:53:46 -05:00
attempts = post . custom_fields [ " rebake_attempts " ] . to_i
2017-12-26 21:51:16 -05:00
2017-12-26 20:44:41 -05:00
if attempts > 3
2018-01-04 17:53:46 -05:00
post . update_columns ( baked_version : BAKED_VERSION )
2018-12-19 11:47:37 -05:00
Discourse . warn_exception ( e , message : " Can not rebake post # #{ post . id } after 3 attempts, giving up " )
2017-12-26 20:44:41 -05:00
else
2018-01-04 17:53:46 -05:00
post . custom_fields [ " rebake_attempts " ] = attempts + 1
post . save_custom_fields
2017-12-26 20:44:41 -05:00
end
2014-05-27 22:30:43 -04:00
end
end
2014-07-17 16:22:46 -04:00
problems
2014-05-27 22:30:43 -04:00
end
2019-01-08 16:57:20 -05: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-27 22:30:43 -04:00
old_cooked = cooked
2019-04-01 04:29:00 -04:00
update_columns (
2019-03-31 22:14:29 -04:00
cooked : new_cooked ,
baked_at : Time . zone . now ,
baked_version : BAKED_VERSION
)
2014-05-27 22:30:43 -04:00
2019-01-08 16:57:20 -05:00
if invalidate_broken_images
2018-12-26 12:52:07 -05:00
custom_fields . delete ( BROKEN_IMAGES )
save_custom_fields
end
2014-05-27 22:30:43 -04:00
# Extracts urls from the body
2014-07-15 03:47:24 -04:00
TopicLink . extract_from ( self )
QuotedPost . extract_from ( self )
2014-05-27 22:30:43 -04:00
# make sure we trigger the post process
2019-01-08 16:57:20 -05:00
trigger_post_process ( bypass_bump : true , priority : priority )
2014-05-27 22:30:43 -04:00
2014-09-22 12:55:13 -04:00
publish_change_to_clients! ( :rebaked )
2014-05-27 22:30:43 -04:00
new_cooked != old_cooked
end
2017-07-27 21:20:09 -04:00
def set_owner ( new_user , actor , skip_revision = false )
2014-10-27 17:06:43 -04:00
return if user_id == new_user . id
2018-08-20 06:26:19 -04:00
edit_reason = I18n . t ( 'change_owner.post_revision_text' , locale : SiteSetting . default_locale )
2017-09-14 10:15:07 -04:00
revise (
actor ,
{ raw : self . raw , user_id : new_user . id , edit_reason : edit_reason } ,
2018-02-27 09:46:20 -05:00
bypass_bump : true , skip_revision : skip_revision , skip_validations : true
2014-10-27 17:06:43 -04:00
)
2016-03-16 08:49:27 -04:00
if post_number == topic . highest_post_number
topic . update_columns ( last_post_user_id : new_user . id )
end
2014-03-27 21:28:14 -04:00
end
2013-02-05 14:16:51 -05:00
before_create do
2013-06-09 12:48:44 -04:00
PostCreator . before_create_tasks ( self )
2013-02-05 14:16:51 -05:00
end
2016-03-29 03:50:17 -04:00
def self . estimate_posts_per_day
2019-12-03 04:05:53 -05:00
val = Discourse . redis . get ( " estimated_posts_per_day " )
2016-03-29 03:50:17 -04: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 04:05:53 -05:00
Discourse . redis . setex ( " estimated_posts_per_day " , 1 . day . to_i , posts_per_day . to_s )
2016-03-29 03:50:17 -04:00
posts_per_day
end
2013-02-07 10:45:24 -05:00
before_save do
2013-02-28 13:54:12 -05:00
self . last_editor_id || = user_id
2016-10-24 00:02:38 -04:00
2017-08-31 00:06:56 -04:00
if ! new_record? && will_save_change_to_raw?
2016-10-24 00:02:38 -04:00
self . cooked = cook ( raw , topic_id : topic_id )
end
2020-03-09 12:37:49 -04:00
self . baked_at = Time . zone . now
2014-05-30 00:45:39 -04:00
self . baked_version = BAKED_VERSION
2013-02-05 14:16:51 -05:00
end
2013-03-18 15:12:31 -04:00
def advance_draft_sequence
return if topic . blank? # could be deleted
2018-07-11 03:06:49 -04:00
DraftSequence . next! ( last_editor_id , topic . draft_key ) if last_editor_id
2013-03-18 15:12:31 -04:00
end
2013-07-22 16:39:20 -04:00
# TODO: move to post-analyzer?
2013-03-18 15:54:08 -04:00
# Determine what posts are quoted by this post
2013-02-05 14:16:51 -05:00
def extract_quoted_post_numbers
2013-05-22 15:45:31 -04:00
temp_collector = [ ]
2013-02-05 14:16:51 -05:00
# Create relationships for the quotes
2013-05-22 15:38:45 -04:00
raw . scan ( / \ [quote= \ "([^"]+)" \ ] / ) . each do | quote |
args = parse_quote_into_arguments ( quote )
2013-05-22 15:45:31 -04:00
# If the topic attribute is present, ensure it's the same topic
2018-07-10 04:17:28 -04:00
if ! ( args [ :topic ] . present? && topic_id != args [ :topic ] ) && args [ :post ] != post_number
temp_collector << args [ :post ]
end
2013-02-05 14:16:51 -05:00
end
2013-02-07 10:45:24 -05:00
2013-05-22 15:45:31 -04:00
temp_collector . uniq!
self . quoted_post_numbers = temp_collector
self . quote_count = temp_collector . size
2013-02-05 14:16:51 -05:00
end
2013-03-18 15:54:08 -04:00
def save_reply_relationships
2013-05-23 12:09:06 -04:00
add_to_quoted_post_numbers ( reply_to_post_number )
return if self . quoted_post_numbers . blank?
2013-03-18 15:54:08 -04:00
# Create a reply relationship between quoted posts and this new post
2013-05-23 12:09:06 -04:00
self . quoted_post_numbers . each do | p |
2014-05-06 09:41:59 -04:00
post = Post . find_by ( topic_id : topic_id , post_number : p )
2013-05-23 12:09:06 -04:00
create_reply_relationship_with ( post )
2013-03-18 15:54:08 -04:00
end
end
2013-03-18 13:55:34 -04:00
# Enqueue post processing for this post
2019-01-16 21:24:32 -05:00
def trigger_post_process ( bypass_bump : false , priority : :normal , new_post : false )
2013-11-21 19:52:26 -05:00
args = {
post_id : id ,
2018-09-05 21:58:01 -04:00
bypass_bump : bypass_bump ,
2019-01-16 21:24:32 -05:00
new_post : new_post ,
2013-11-21 19:52:26 -05:00
}
2013-02-28 13:54:12 -05:00
args [ :image_sizes ] = image_sizes if image_sizes . present?
args [ :invalidate_oneboxes ] = true if invalidate_oneboxes . present?
2015-09-29 12:51:26 -04:00
args [ :cooking_options ] = self . cooking_options
2019-01-08 16:57:20 -05:00
2019-01-16 22:53:09 -05:00
if priority && priority != :normal
args [ :queue ] = priority . to_s
2019-01-08 16:57:20 -05:00
end
2013-02-07 10:45:24 -05:00
Jobs . enqueue ( :process_post , args )
2015-09-03 23:35:25 -04:00
DiscourseEvent . trigger ( :after_trigger_post_process , self )
2013-02-05 14:16:51 -05:00
end
2013-03-07 11:07:59 -05:00
2020-04-22 04:52:50 -04: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-02 18:24:43 -04:00
. where ( post_type : Post . types [ :regular ] )
2020-04-22 04:52:50 -04: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 03:29:17 -04:00
result
. group ( 'date(posts.created_at)' )
. order ( 'date(posts.created_at)' )
. count
2013-04-03 13:25:52 -04:00
end
2016-04-21 05:22:41 -04:00
def self . private_messages_count_per_day ( start_date , end_date , topic_subtype )
2018-06-05 03:29:17 -04: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-07 11:07:59 -05:00
end
2013-05-17 12:15:21 -04:00
2017-07-27 21:20:09 -04:00
def reply_history ( max_replies = 100 , guardian = nil )
2018-06-19 02:13:14 -04: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-26 18:44:42 -04:00
# [1,2,3][-10,-1] => nil
2017-07-27 21:20:09 -04:00
post_ids = ( post_ids [ ( 0 - max_replies ) .. - 1 ] || post_ids )
2014-10-26 18:44:42 -04:00
2015-09-24 20:15:58 -04:00
Post . secured ( guardian ) . where ( id : post_ids ) . includes ( :user , :topic ) . order ( :id ) . to_a
2013-08-06 17:42:36 -04:00
end
2017-12-14 18:23:51 -05:00
MAX_REPLY_LEVEL || = 1000
2018-04-20 17:05:51 -04:00
def reply_ids ( guardian = nil , only_replies_to_single_post : true )
2018-06-20 03:48:02 -04:00
builder = DB . build ( << ~ SQL )
2017-12-13 18:43:48 -05:00
WITH RECURSIVE breadcrumb ( id , level ) AS (
SELECT :post_id , 0
2017-12-13 16:12:06 -05:00
UNION
2020-01-17 11:24:49 -05:00
SELECT reply_post_id , level + 1
2018-04-20 17:05:51 -04:00
FROM post_replies AS r
2020-02-03 13:12:27 -05:00
JOIN posts AS p ON p . id = reply_post_id
2018-04-20 17:05:51 -04:00
JOIN breadcrumb AS b ON ( r . post_id = b . id )
2020-01-17 11:24:49 -05:00
WHERE r . post_id < > r . reply_post_id
2020-02-03 13:12:27 -05:00
AND b . level < :max_reply_level
AND p . topic_id = :topic_id
2017-12-13 18:43:48 -05:00
) , breadcrumb_with_count AS (
2018-04-20 17:05:51 -04:00
SELECT
id ,
level ,
COUNT ( * ) AS count
FROM post_replies AS r
2020-01-17 11:24:49 -05:00
JOIN breadcrumb AS b ON ( r . reply_post_id = b . id )
WHERE r . reply_post_id < > r . post_id
2018-04-20 17:05:51 -04:00
GROUP BY id , level
2017-12-13 18:43:48 -05:00
)
2020-02-03 13:34:35 -05:00
SELECT id , MIN ( level ) AS level
2018-04-20 17:05:51 -04:00
FROM breadcrumb_with_count
/ *where* /
2020-02-03 13:34:35 -05:00
GROUP BY id
2018-04-20 17:05:51 -04:00
ORDER BY id
SQL
2017-12-13 16:12:06 -05:00
2018-04-20 17:05:51 -04: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-03 13:12:27 -05:00
replies = builder . query_hash ( post_id : id , max_reply_level : MAX_REPLY_LEVEL , topic_id : topic_id )
2018-06-20 03:48:02 -04:00
replies . each { | r | r . symbolize_keys! }
2017-12-13 16:12:06 -05:00
secured_ids = Post . secured ( guardian ) . where ( id : replies . map { | r | r [ :id ] } ) . pluck ( :id ) . to_set
2017-12-13 18:43:48 -05:00
replies . reject { | r | ! secured_ids . include? ( r [ :id ] ) }
2017-12-13 16:12:06 -05:00
end
2013-12-11 21:41:34 -05:00
def revert_to ( number )
return if number > = version
2014-05-06 09:41:59 -04:00
post_revision = PostRevision . find_by ( post_id : id , number : ( number + 1 ) )
2013-12-11 21:41:34 -05:00
post_revision . modifications . each do | attribute , change |
attribute = " version " if attribute == " cached_version "
write_attribute ( attribute , change [ 0 ] )
end
end
2013-05-17 12:15:21 -04:00
2015-04-24 05:14:10 -04:00
def self . rebake_all_quoted_posts ( user_id )
return if user_id . blank?
2018-06-19 02:13:14 -04:00
DB . exec ( << ~ SQL , user_id )
2015-04-24 05:14:10 -04:00
WITH user_quoted_posts AS (
SELECT post_id
FROM quoted_posts
2018-06-19 02:13:14 -04:00
WHERE quoted_post_id IN ( SELECT id FROM posts WHERE user_id = ?)
2015-04-24 05:14:10 -04: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-26 20:19:49 -05:00
def seen? ( user )
PostTiming . where ( topic_id : topic_id , post_number : post_number , user_id : user . id ) . exists?
end
2016-12-21 21:13:14 -05:00
def index_search
SearchIndexer . index ( self )
end
2018-01-25 15:38:40 -05:00
def locked?
locked_by_id . present?
end
2018-09-05 21:58:01 -04:00
def link_post_uploads ( fragments : nil )
upload_ids = [ ]
2019-05-03 15:46:20 -04: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-05 21:58:01 -04:00
end
upload_ids |= Upload . where ( id : downloaded_images . values ) . pluck ( :id )
2020-01-15 22:50:27 -05:00
post_uploads = upload_ids . map do | upload_id |
{ post_id : self . id , upload_id : upload_id }
2019-11-17 20:25:42 -05:00
end
2018-09-05 21:58:01 -04:00
PostUpload . transaction do
PostUpload . where ( post_id : self . id ) . delete_all
2020-01-15 22:50:27 -05:00
if post_uploads . size > 0
PostUpload . insert_all ( post_uploads )
end
if SiteSetting . secure_media?
2020-02-13 20:17:09 -05:00
Upload . where (
id : upload_ids , access_control_post_id : nil
2020-03-02 00:40:29 -05:00
) . where (
'id NOT IN (SELECT upload_id FROM custom_emojis)'
2020-02-13 20:17:09 -05:00
) . update_all (
2020-01-15 22:50:27 -05:00
access_control_post_id : self . id
)
2018-09-05 21:58:01 -04:00
end
end
end
2019-11-17 20:25:42 -05:00
def update_uploads_secure_status
if Discourse . store . external?
self . uploads . each { | upload | upload . update_secure_status }
end
end
2018-09-05 21:58:01 -04:00
def downloaded_images
JSON . parse ( self . custom_fields [ Post :: DOWNLOADED_IMAGES ] . presence || " {} " )
rescue JSON :: ParserError
{ }
end
2019-07-29 02:35:34 -04:00
def each_upload_url ( fragments : nil , include_local_upload : true )
2019-06-24 15:49:58 -04:00
current_db = RailsMultisite :: ConnectionManagement . current_db
2019-05-03 15:46:20 -04:00
upload_patterns = [
2019-06-24 15:49:58 -04:00
/ \/ uploads \/ #{ current_db } \/ / ,
2019-05-03 15:46:20 -04:00
/ \/ original \/ / ,
2019-05-28 21:00:25 -04:00
/ \/ optimized \/ / ,
2019-06-18 21:10:50 -04:00
/ \/ uploads \/ short-url \/ [a-zA-Z0-9]+( \ .[a-z0-9]+)? /
2019-05-03 15:46:20 -04:00
]
2019-05-28 21:00:25 -04:00
2020-05-04 23:46:57 -04:00
fragments || = Nokogiri :: HTML5 :: fragment ( self . cooked )
2020-03-10 09:01:40 -04:00
selectors = fragments . css ( " a/@href " , " img/@src " , " source/@src " , " track/@src " , " video/@poster " )
2019-11-17 20:25:42 -05:00
2020-03-10 09:01:40 -04:00
links = selectors . map do | media |
2019-09-02 05:41:22 -04: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-03 15:46:20 -04:00
links . each do | src |
2019-09-02 05:41:22 -04: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 06:02:28 -04:00
elsif src . include? ( " /uploads/short-url/ " )
sha1 = Upload . sha1_from_short_path ( src )
yield ( src , nil , sha1 )
next
2019-09-02 05:41:22 -04:00
end
next if upload_patterns . none? { | pattern | src =~ pattern }
2019-09-22 06:02:28 -04:00
next if Rails . configuration . multisite && src . exclude? ( current_db )
2019-05-03 15:46:20 -04:00
src = " #{ SiteSetting . force_https ? " https " : " http " } : #{ src } " if src . start_with? ( " // " )
2019-07-29 08:02:18 -04:00
next unless Discourse . store . has_been_uploaded? ( src ) || ( include_local_upload && src =~ / \ A \/ [^ \/ ] /i )
2019-05-03 15:46:20 -04:00
path = begin
2019-12-11 21:49:21 -05:00
URI ( UrlHelper . unencode ( GlobalSetting . cdn_url ? src . sub ( GlobalSetting . cdn_url , " " ) : src ) ) & . path
2019-05-03 15:46:20 -04:00
rescue URI :: Error
end
next if path . blank?
sha1 =
if path . include? " optimized "
OptimizedImage . extract_sha1 ( path )
else
2019-05-28 21:00:25 -04:00
Upload . extract_sha1 ( path ) || Upload . sha1_from_short_path ( path )
2019-05-03 15:46:20 -04:00
end
yield ( src , path , sha1 )
end
end
2019-07-29 02:35:34 -04:00
def self . find_missing_uploads ( include_local_upload : true )
2019-05-03 15:46:20 -04:00
missing_uploads = [ ]
missing_post_uploads = { }
2019-05-16 05:47:53 -04:00
count = 0
2019-05-03 15:46:20 -04:00
2019-08-14 21:48:08 -04:00
DistributedMutex . synchronize ( " find_missing_uploads " , validity : 30 . minutes ) do
2019-05-16 05:47:53 -04: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 06:32:27 -04:00
upload_id = Upload . where ( sha1 : sha1 ) . pluck_first ( :id ) if sha1 . present?
2019-05-16 05:47:53 -04:00
upload_id || = yield ( post , src , path , sha1 )
2019-07-18 16:14:08 -04:00
if upload_id . blank?
2019-05-16 05:47:53 -04:00
missing_uploads << src
missing_post_uploads [ post . id ] << src
end
2019-05-03 15:46:20 -04:00
end
end
end
2019-05-16 05:47:53 -04: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 00:34:04 -04:00
2019-05-16 05:47:53 -04:00
uploads . empty?
end
2019-05-03 15:46:20 -04:00
end
2019-05-16 00:34:04 -04:00
{ uploads : missing_uploads , post_uploads : missing_post_uploads , count : count }
2019-05-03 15:46:20 -04:00
end
2020-01-22 21:01:10 -05: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 04:07:50 -04:00
def image_url
image_upload & . url
end
2013-12-11 21:41:34 -05:00
private
2013-05-22 15:45:31 -04:00
2013-05-22 15:38:45 -04:00
def parse_quote_into_arguments ( quote )
return { } unless quote . present?
2014-08-17 23:00:02 -04:00
args = HashWithIndifferentAccess . new
2013-05-22 15:38:45 -04:00
quote . first . scan ( / ([a-z]+) \ :( \ d+) / ) . each do | arg |
2014-08-17 23:00:02 -04:00
args [ arg [ 0 ] ] = arg [ 1 ] . to_i
2013-05-22 15:38:45 -04:00
end
args
end
2013-05-22 15:45:31 -04:00
2013-05-23 12:07:45 -04:00
def add_to_quoted_post_numbers ( num )
return unless num . present?
self . quoted_post_numbers || = [ ]
self . quoted_post_numbers << num
end
2013-05-23 12:08:24 -04:00
def create_reply_relationship_with ( post )
2018-05-16 11:02:43 -04:00
return if post . nil? || self . deleted_at . present?
2020-01-17 11:24:49 -05:00
post_reply = post . post_replies . new ( reply_post_id : id )
2013-05-23 12:08:24 -04:00
if post_reply . save
2015-09-24 20:15:58 -04: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-23 12:08:24 -04:00
end
end
2013-02-05 14:16:51 -05:00
end
2013-05-23 22:48:32 -04:00
# == Schema Information
#
# Table name: posts
#
# id :integer not null, primary key
2013-09-03 17:19:29 -04:00
# user_id :integer
2013-05-23 22:48:32 -04:00
# topic_id :integer not null
# post_number :integer not null
# raw :text not null
# cooked :text not null
2014-08-27 01:30:17 -04:00
# created_at :datetime not null
# updated_at :datetime not null
2013-05-23 22:48:32 -04: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-16 20:48:58 -04:00
# like_score :integer default(0), not null
2013-07-13 21:24:16 -04:00
# deleted_by_id :integer
2019-01-11 14:29:56 -05:00
# edit_reason :string
2014-02-06 19:07:36 -05:00
# word_count :integer
# version :integer default(1), not null
# cook_method :integer default(1), not null
2014-05-21 19:00:38 -04:00
# wiki :boolean default(FALSE), not null
2014-05-29 00:59:14 -04:00
# baked_at :datetime
2014-07-03 03:29:44 -04:00
# baked_version :integer
# hidden_at :datetime
2014-07-14 21:29:44 -04:00
# self_edits :integer default(0), not null
2014-07-30 23:14:40 -04:00
# reply_quoted :boolean default(FALSE), not null
2014-11-19 22:53:15 -05:00
# via_email :boolean default(FALSE), not null
# raw_email :text
# public_version :integer default(1), not null
2019-01-11 14:29:56 -05:00
# action_code :string
2016-10-31 05:41:33 -04:00
# image_url :string
2018-02-20 01:28:58 -05: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 04:07:50 -04:00
# image_upload_id :bigint
2013-05-23 22:48:32 -04:00
#
# Indexes
#
2018-07-16 02:18:07 -04: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 00:37:11 -04:00
# index_for_rebake_old (id) WHERE (((baked_version IS NULL) OR (baked_version < 2)) AND (deleted_at IS NULL))
2019-04-09 01:27:22 -04: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 04:07:50 -04:00
# index_posts_on_image_upload_id (image_upload_id)
2018-07-16 02:18:07 -04: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-23 22:48:32 -04:00
#