railspress-engine 1.4.2 → 1.4.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/README.md +10 -0
- data/app/controllers/railspress/admin/imports_controller.rb +10 -1
- data/app/controllers/railspress/admin/posts_controller.rb +3 -3
- data/app/controllers/railspress/api/v1/posts_controller.rb +2 -2
- data/app/jobs/railspress/import_posts_job.rb +7 -4
- data/app/models/railspress/post.rb +10 -2
- data/app/models/railspress/post_import_processor.rb +26 -17
- data/lib/railspress/engine.rb +4 -0
- data/lib/railspress/entity.rb +19 -3
- data/lib/railspress/version.rb +1 -1
- data/lib/railspress.rb +5 -0
- metadata +16 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 48860d966cd3cdc8832ba51098ea94bf8b7264608105231af1b7871d4576d1c8
|
|
4
|
+
data.tar.gz: e4e9e1631fa9127c0c221e08c081e5280e85a7f7f7f3716814033f722aac921b
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 0523661e660f0d5490b95282ed786669282ea2edfe29a84397fafd10c22e1c599d383a9a40c4b3e2c1e9efa4476a82e24a5236a231c44d27041355986a100803
|
|
7
|
+
data.tar.gz: 699ae2eba634ef60e19f7c18b8f98bbd3470c61dcaa750eeff7d6ece5a07436bd33ab19e37740540f93f3621c3961bb3066abcb650717d1658cbc8b24232a97d
|
data/README.md
CHANGED
|
@@ -157,6 +157,16 @@ end
|
|
|
157
157
|
|
|
158
158
|
See [CONFIGURING.md](docs/CONFIGURING.md) for more authentication patterns including Devise integration.
|
|
159
159
|
|
|
160
|
+
## Import Security
|
|
161
|
+
|
|
162
|
+
RailsPress stores imported files inside engine-owned temporary directories.
|
|
163
|
+
|
|
164
|
+
ZIP image references cannot escape their extraction directory.
|
|
165
|
+
|
|
166
|
+
Remote header images accept public HTTP and HTTPS destinations. Private and redirected destinations are rejected.
|
|
167
|
+
|
|
168
|
+
Configure request and worker limits for additional availability protection.
|
|
169
|
+
|
|
160
170
|
## Quick Start
|
|
161
171
|
|
|
162
172
|
Access the admin at `/railspress/admin`. From there you can manage posts, entities, and blocks.
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
module Railspress
|
|
2
2
|
module Admin
|
|
3
3
|
class ImportsController < BaseController
|
|
4
|
+
SUPPORTED_IMPORT_EXTENSIONS = %w[.md .markdown .txt .zip].freeze
|
|
5
|
+
|
|
4
6
|
before_action :validate_import_type, only: [ :show ]
|
|
5
7
|
|
|
6
8
|
def show
|
|
@@ -53,11 +55,18 @@ module Railspress
|
|
|
53
55
|
FileUtils.mkdir_p(upload_dir)
|
|
54
56
|
|
|
55
57
|
uploaded_files.map do |file|
|
|
56
|
-
path = upload_dir.join(file
|
|
58
|
+
path = upload_dir.join(stored_upload_name(file))
|
|
57
59
|
File.open(path, "wb") { |f| f.write(file.read) }
|
|
58
60
|
path.to_s
|
|
59
61
|
end
|
|
60
62
|
end
|
|
63
|
+
|
|
64
|
+
def stored_upload_name(file)
|
|
65
|
+
extension = File.extname(file.original_filename.to_s).downcase
|
|
66
|
+
extension = "" unless SUPPORTED_IMPORT_EXTENSIONS.include?(extension)
|
|
67
|
+
|
|
68
|
+
"#{SecureRandom.hex(16)}#{extension}"
|
|
69
|
+
end
|
|
61
70
|
end
|
|
62
71
|
end
|
|
63
72
|
end
|
|
@@ -9,15 +9,15 @@ module Railspress
|
|
|
9
9
|
@direction = params[:direction].presence || "desc"
|
|
10
10
|
|
|
11
11
|
@posts = Post.includes(:category, :tags)
|
|
12
|
-
.
|
|
12
|
+
.rp_search(params[:q])
|
|
13
13
|
.by_category(params[:category_id])
|
|
14
14
|
.by_status(params[:status])
|
|
15
15
|
.sorted_by(@sort, @direction)
|
|
16
16
|
|
|
17
17
|
@total_count = @posts.count
|
|
18
18
|
@page = [ params[:page].to_i, 1 ].max
|
|
19
|
-
@total_pages = (@total_count.to_f / Post
|
|
20
|
-
@posts = @posts.
|
|
19
|
+
@total_pages = (@total_count.to_f / Post.rp_per_page_count).ceil
|
|
20
|
+
@posts = @posts.rp_page(@page)
|
|
21
21
|
|
|
22
22
|
@categories = Category.ordered
|
|
23
23
|
end
|
|
@@ -102,8 +102,8 @@ module Railspress
|
|
|
102
102
|
end
|
|
103
103
|
|
|
104
104
|
def per_page
|
|
105
|
-
requested = params.fetch(:per, Railspress::Post.
|
|
106
|
-
requested = Railspress::Post.
|
|
105
|
+
requested = params.fetch(:per, Railspress::Post.rp_per_page_count).to_i
|
|
106
|
+
requested = Railspress::Post.rp_per_page_count if requested <= 0
|
|
107
107
|
[ requested, 100 ].min
|
|
108
108
|
end
|
|
109
109
|
|
|
@@ -30,12 +30,15 @@ module Railspress
|
|
|
30
30
|
end
|
|
31
31
|
|
|
32
32
|
def cleanup_uploaded_files(file_paths)
|
|
33
|
-
tmp_dir = Rails.root.join("tmp")
|
|
33
|
+
tmp_dir = File.realpath(Rails.root.join("tmp"))
|
|
34
34
|
|
|
35
35
|
file_paths.each do |path|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
36
|
+
next unless File.exist?(path)
|
|
37
|
+
|
|
38
|
+
resolved_path = File.realpath(path)
|
|
39
|
+
next unless resolved_path.start_with?("#{tmp_dir}#{File::SEPARATOR}")
|
|
40
|
+
|
|
41
|
+
FileUtils.rm_f(resolved_path)
|
|
39
42
|
end
|
|
40
43
|
rescue => e
|
|
41
44
|
Rails.logger.warn "Failed to cleanup uploaded import files: #{e.message}"
|
|
@@ -74,14 +74,22 @@ module Railspress
|
|
|
74
74
|
before_save :set_published_at
|
|
75
75
|
before_save :set_reading_time, if: -> { reading_time.blank? && content.present? }
|
|
76
76
|
|
|
77
|
-
# Generic scopes (ordered, recent) and pagination (
|
|
77
|
+
# Generic scopes (ordered, recent) and pagination (rp_page) provided by Entity concern
|
|
78
78
|
# Post-specific scopes below:
|
|
79
79
|
scope :published, -> { where(status: :published).where(published_at: ..Time.current) }
|
|
80
80
|
scope :drafts, -> { where(status: :draft) }
|
|
81
81
|
scope :scheduled, -> { where(status: :published).where("published_at > ?", Time.current) }
|
|
82
82
|
scope :live, -> { published } # Alias for semantic clarity
|
|
83
83
|
scope :by_author, ->(author) { where(author_id: author.id) }
|
|
84
|
-
scope :
|
|
84
|
+
scope :rp_search, ->(query) {
|
|
85
|
+
query.present? ? where(arel_table[:title].matches("%#{query}%", nil, false)) : all
|
|
86
|
+
}
|
|
87
|
+
unless respond_to?(:search)
|
|
88
|
+
scope :search, ->(query) {
|
|
89
|
+
Railspress.deprecator.warn("search is deprecated and will be removed in RailsPress 2.0; use rp_search instead")
|
|
90
|
+
rp_search(query)
|
|
91
|
+
}
|
|
92
|
+
end
|
|
85
93
|
scope :by_category, ->(category_id) { where(category_id: category_id) if category_id.present? }
|
|
86
94
|
scope :by_status, ->(status) {
|
|
87
95
|
case status.to_s
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
require "yaml"
|
|
2
2
|
require "zip"
|
|
3
|
-
require "open-uri"
|
|
4
3
|
require "fileutils"
|
|
5
4
|
require "redcarpet"
|
|
5
|
+
require "ssrf_filter"
|
|
6
|
+
require "stringio"
|
|
6
7
|
|
|
7
8
|
module Railspress
|
|
8
9
|
class PostImportProcessor
|
|
@@ -211,29 +212,20 @@ module Railspress
|
|
|
211
212
|
filename = File.basename(uri.path)
|
|
212
213
|
filename = "header_image#{File.extname(uri.path)}" if filename.blank?
|
|
213
214
|
|
|
214
|
-
|
|
215
|
+
response = SsrfFilter.get(url, max_redirects: 0)
|
|
216
|
+
response.value
|
|
215
217
|
post.header_image.attach(
|
|
216
|
-
io:
|
|
218
|
+
io: StringIO.new(response.body),
|
|
217
219
|
filename: filename,
|
|
218
|
-
content_type:
|
|
220
|
+
content_type: response.content_type
|
|
219
221
|
)
|
|
220
|
-
rescue
|
|
222
|
+
rescue SsrfFilter::Error, URI::InvalidURIError, Net::HTTPError => e
|
|
221
223
|
Rails.logger.warn "Failed to download header image from #{url}: #{e.message}"
|
|
222
224
|
end
|
|
223
225
|
|
|
224
226
|
def attach_image_from_zip(post, relative_path, source_path, base_dir)
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
# Try relative to the markdown file first
|
|
229
|
-
image_path = File.expand_path(relative_path, source_dir)
|
|
230
|
-
|
|
231
|
-
# If not found, try relative to base_dir
|
|
232
|
-
unless File.exist?(image_path)
|
|
233
|
-
image_path = File.expand_path(relative_path, base_dir)
|
|
234
|
-
end
|
|
235
|
-
|
|
236
|
-
return unless File.exist?(image_path)
|
|
227
|
+
image_path = imported_image_path(relative_path, source_path, base_dir)
|
|
228
|
+
return unless image_path
|
|
237
229
|
|
|
238
230
|
ext = File.extname(image_path).downcase
|
|
239
231
|
return unless IMAGE_EXTENSIONS.include?(ext)
|
|
@@ -253,6 +245,23 @@ module Railspress
|
|
|
253
245
|
)
|
|
254
246
|
end
|
|
255
247
|
|
|
248
|
+
def imported_image_path(relative_path, source_path, base_dir)
|
|
249
|
+
import_root = File.realpath(base_dir)
|
|
250
|
+
source_dir = File.dirname(source_path)
|
|
251
|
+
|
|
252
|
+
[ source_dir, import_root ].each do |directory|
|
|
253
|
+
candidate = File.expand_path(relative_path.to_s, directory)
|
|
254
|
+
next unless File.file?(candidate)
|
|
255
|
+
|
|
256
|
+
resolved_candidate = File.realpath(candidate)
|
|
257
|
+
return resolved_candidate if resolved_candidate.start_with?("#{import_root}#{File::SEPARATOR}")
|
|
258
|
+
end
|
|
259
|
+
|
|
260
|
+
nil
|
|
261
|
+
rescue Errno::EACCES, Errno::ENOENT
|
|
262
|
+
nil
|
|
263
|
+
end
|
|
264
|
+
|
|
256
265
|
def extract_title_from_body(body)
|
|
257
266
|
# Look for first H1: # Title
|
|
258
267
|
if match = body.match(/^#\s+(.+)$/)
|
data/lib/railspress/engine.rb
CHANGED
|
@@ -2,6 +2,10 @@ module Railspress
|
|
|
2
2
|
class Engine < ::Rails::Engine
|
|
3
3
|
isolate_namespace Railspress
|
|
4
4
|
|
|
5
|
+
initializer "railspress.deprecator" do |app|
|
|
6
|
+
app.deprecators[:railspress] = Railspress.deprecator
|
|
7
|
+
end
|
|
8
|
+
|
|
5
9
|
# Register engine assets with the asset pipeline (Propshaft)
|
|
6
10
|
initializer "railspress.assets" do |app|
|
|
7
11
|
if app.config.respond_to?(:assets)
|
data/lib/railspress/entity.rb
CHANGED
|
@@ -149,6 +149,22 @@ module Railspress
|
|
|
149
149
|
# Default scopes available to all entities
|
|
150
150
|
scope :ordered, -> { order(created_at: :desc) }
|
|
151
151
|
scope :recent, -> { ordered.limit(10) }
|
|
152
|
+
|
|
153
|
+
unless respond_to?(:page)
|
|
154
|
+
scope :page, ->(page_number) {
|
|
155
|
+
Railspress.deprecator.warn("page is deprecated and will be removed in RailsPress 2.0; use rp_page instead")
|
|
156
|
+
rp_page(page_number)
|
|
157
|
+
}
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
unless respond_to?(:per_page_count)
|
|
161
|
+
define_singleton_method(:per_page_count) do
|
|
162
|
+
Railspress.deprecator.warn(
|
|
163
|
+
"per_page_count is deprecated and will be removed in RailsPress 2.0; use rp_per_page_count instead"
|
|
164
|
+
)
|
|
165
|
+
rp_per_page_count
|
|
166
|
+
end
|
|
167
|
+
end
|
|
152
168
|
end
|
|
153
169
|
|
|
154
170
|
# Default pagination - can be overridden in model
|
|
@@ -156,13 +172,13 @@ module Railspress
|
|
|
156
172
|
|
|
157
173
|
class_methods do
|
|
158
174
|
# Simple pagination for index views
|
|
159
|
-
def
|
|
175
|
+
def rp_page(page_number)
|
|
160
176
|
page_number = [ page_number.to_i, 1 ].max
|
|
161
|
-
offset((page_number - 1) *
|
|
177
|
+
offset((page_number - 1) * rp_per_page_count).limit(rp_per_page_count)
|
|
162
178
|
end
|
|
163
179
|
|
|
164
180
|
# Allow models to override PER_PAGE
|
|
165
|
-
def
|
|
181
|
+
def rp_per_page_count
|
|
166
182
|
const_defined?(:PER_PAGE, false) ? const_get(:PER_PAGE) : Railspress::Entity::PER_PAGE
|
|
167
183
|
end
|
|
168
184
|
|
data/lib/railspress/version.rb
CHANGED
data/lib/railspress.rb
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
require "railspress/version"
|
|
2
|
+
require "active_support/deprecation"
|
|
2
3
|
require "railspress/cms_helper"
|
|
3
4
|
require "railspress/engine"
|
|
4
5
|
require "railspress/entity"
|
|
@@ -195,6 +196,10 @@ module Railspress
|
|
|
195
196
|
end
|
|
196
197
|
|
|
197
198
|
class << self
|
|
199
|
+
def deprecator
|
|
200
|
+
@deprecator ||= ActiveSupport::Deprecation.new("2.0", "RailsPress")
|
|
201
|
+
end
|
|
202
|
+
|
|
198
203
|
def configuration
|
|
199
204
|
@configuration ||= Configuration.new
|
|
200
205
|
end
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: railspress-engine
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 1.4.
|
|
4
|
+
version: 1.4.4
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Avi Flombaum
|
|
@@ -85,6 +85,20 @@ dependencies:
|
|
|
85
85
|
- - ">="
|
|
86
86
|
- !ruby/object:Gem::Version
|
|
87
87
|
version: '3.6'
|
|
88
|
+
- !ruby/object:Gem::Dependency
|
|
89
|
+
name: ssrf_filter
|
|
90
|
+
requirement: !ruby/object:Gem::Requirement
|
|
91
|
+
requirements:
|
|
92
|
+
- - "~>"
|
|
93
|
+
- !ruby/object:Gem::Version
|
|
94
|
+
version: '1.5'
|
|
95
|
+
type: :runtime
|
|
96
|
+
prerelease: false
|
|
97
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
98
|
+
requirements:
|
|
99
|
+
- - "~>"
|
|
100
|
+
- !ruby/object:Gem::Version
|
|
101
|
+
version: '1.5'
|
|
88
102
|
- !ruby/object:Gem::Dependency
|
|
89
103
|
name: rspec-rails
|
|
90
104
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -372,7 +386,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
372
386
|
- !ruby/object:Gem::Version
|
|
373
387
|
version: '0'
|
|
374
388
|
requirements: []
|
|
375
|
-
rubygems_version: 4.0.
|
|
389
|
+
rubygems_version: 4.0.18
|
|
376
390
|
specification_version: 4
|
|
377
391
|
summary: A mountable blog + CMS engine for Rails 8+
|
|
378
392
|
test_files: []
|