Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 25 additions & 0 deletions lib/bunko/configuration.rb
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,9 @@ def post_type(name, title: nil, &block)
block.call(customizer)
end

# Validate title after the block runs so titles set via the customizer are covered too
validate_title!(post_type[:title], "PostType")

@post_types << post_type
end

Expand Down Expand Up @@ -135,6 +138,9 @@ def collection(name, title: nil, post_types: nil, scope: nil, &block)
block.call(customizer)
end

# Validate title after the block runs so titles set via the customizer are covered too
validate_title!(collection[:title], "Collection")

# Validate that post_types was set
if collection[:post_types].empty?
raise ArgumentError, "Collection '#{name_str}' must specify at least one post_type"
Expand All @@ -153,6 +159,25 @@ def find_collection(name)

private

# Titles are interpolated into generated view code (e.g., link_to "Title" in the
# shared nav partial), so characters that could corrupt or inject code into the
# generated ERB templates are not allowed.
def validate_title!(title, label)
title_str = title.to_s

if title_str.include?('"')
raise ArgumentError, "#{label} title '#{title_str}' cannot contain double quotes. Titles are used in generated view templates (e.g., link_to \"Title\", ...). Use single quotes or typographic quotes instead."
end

if title_str.include?("<%") || title_str.include?("%>")
raise ArgumentError, "#{label} title '#{title_str}' cannot contain ERB delimiters ('<%' or '%>'). Titles are used in generated view templates."
end

if title_str.match?(/[\r\n]/)
raise ArgumentError, "#{label} title cannot contain newlines or carriage returns. Titles are used in generated view templates."
end
end

def post_type_exists?(name)
@post_types.any? { |pt| pt[:name] == name.to_s }
end
Expand Down
302 changes: 302 additions & 0 deletions test/configuration/title_validation_test.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,302 @@
# frozen_string_literal: true

require_relative "../test_helper"

class TitleValidationTest < ActiveSupport::TestCase
def setup
# Reset configuration before each test
Bunko.reset_configuration!
end

def teardown
# Clean up after each test
Bunko.reset_configuration!
end

# PostType title validation (keyword argument)

test "post_type rejects title with double quotes" do
error = assert_raises(ArgumentError) do
Bunko.configure do |config|
config.post_type "blog", title: 'My "Great" Blog'
end
end

assert_match(/cannot contain double quotes/, error.message)
end

test "post_type rejects title with ERB opening delimiter" do
error = assert_raises(ArgumentError) do
Bunko.configure do |config|
config.post_type "blog", title: "Blog <%= User.destroy_all %>"
end
end

assert_match(/cannot contain ERB delimiters/, error.message)
end

test "post_type rejects title with ERB closing delimiter" do
error = assert_raises(ArgumentError) do
Bunko.configure do |config|
config.post_type "blog", title: "Blog %> injection"
end
end

assert_match(/cannot contain ERB delimiters/, error.message)
end

test "post_type rejects title with newline" do
error = assert_raises(ArgumentError) do
Bunko.configure do |config|
config.post_type "blog", title: "Blog\nInjected"
end
end

assert_match(/cannot contain newlines/, error.message)
end

test "post_type rejects title with carriage return" do
error = assert_raises(ArgumentError) do
Bunko.configure do |config|
config.post_type "blog", title: "Blog\rInjected"
end
end

assert_match(/cannot contain newlines/, error.message)
end

# PostType title validation (customizer block)

test "post_type rejects title with double quotes set via block" do
error = assert_raises(ArgumentError) do
Bunko.configure do |config|
config.post_type "blog" do |type|
type.title = 'My "Great" Blog'
end
end
end

assert_match(/cannot contain double quotes/, error.message)
end

test "post_type rejects title with ERB delimiters set via block" do
error = assert_raises(ArgumentError) do
Bunko.configure do |config|
config.post_type "blog" do |type|
type.title = "<%= system('rm -rf') %>"
end
end
end

assert_match(/cannot contain ERB delimiters/, error.message)
end

test "post_type rejects title with newline set via block" do
error = assert_raises(ArgumentError) do
Bunko.configure do |config|
config.post_type "blog" do |type|
type.title = "Blog\nInjected"
end
end
end

assert_match(/cannot contain newlines/, error.message)
end

test "invalid post_type title is not added to configuration" do
assert_raises(ArgumentError) do
Bunko.configure do |config|
config.post_type "blog", title: 'Bad "Title"'
end
end

assert_empty Bunko.configuration.post_types
end

# Collection title validation (keyword argument)

test "collection rejects title with double quotes" do
error = assert_raises(ArgumentError) do
Bunko.configure do |config|
config.post_type "articles"
config.collection "resources", title: 'The "Best" Resources', post_types: ["articles"]
end
end

assert_match(/cannot contain double quotes/, error.message)
end

test "collection rejects title with ERB opening delimiter" do
error = assert_raises(ArgumentError) do
Bunko.configure do |config|
config.post_type "articles"
config.collection "resources", title: "Resources <% evil %>", post_types: ["articles"]
end
end

assert_match(/cannot contain ERB delimiters/, error.message)
end

test "collection rejects title with ERB closing delimiter" do
error = assert_raises(ArgumentError) do
Bunko.configure do |config|
config.post_type "articles"
config.collection "resources", title: "Resources %>", post_types: ["articles"]
end
end

assert_match(/cannot contain ERB delimiters/, error.message)
end

test "collection rejects title with newline" do
error = assert_raises(ArgumentError) do
Bunko.configure do |config|
config.post_type "articles"
config.collection "resources", title: "Resources\nInjected", post_types: ["articles"]
end
end

assert_match(/cannot contain newlines/, error.message)
end

test "collection rejects title with carriage return" do
error = assert_raises(ArgumentError) do
Bunko.configure do |config|
config.post_type "articles"
config.collection "resources", title: "Resources\r\nInjected", post_types: ["articles"]
end
end

assert_match(/cannot contain newlines/, error.message)
end

# Collection title validation (customizer block)

test "collection rejects title with double quotes set via block" do
error = assert_raises(ArgumentError) do
Bunko.configure do |config|
config.post_type "articles"
config.collection "resources" do |c|
c.title = 'The "Best" Resources'
c.post_types = ["articles"]
end
end
end

assert_match(/cannot contain double quotes/, error.message)
end

test "collection rejects title with ERB delimiters set via block" do
error = assert_raises(ArgumentError) do
Bunko.configure do |config|
config.post_type "articles"
config.collection "resources" do |c|
c.title = "<%= Post.delete_all %>"
c.post_types = ["articles"]
end
end
end

assert_match(/cannot contain ERB delimiters/, error.message)
end

test "collection rejects title with newline set via block" do
error = assert_raises(ArgumentError) do
Bunko.configure do |config|
config.post_type "articles"
config.collection "resources" do |c|
c.title = "Resources\nInjected"
c.post_types = ["articles"]
end
end
end

assert_match(/cannot contain newlines/, error.message)
end

test "invalid collection title is not added to configuration" do
assert_raises(ArgumentError) do
Bunko.configure do |config|
config.post_type "articles"
config.collection "resources", title: 'Bad "Title"', post_types: ["articles"]
end
end

assert_empty Bunko.configuration.collections
end

# Safe titles still work

test "post_type accepts title with apostrophe" do
Bunko.configure do |config|
config.post_type "blog", title: "Kane's Blog"
end

assert_equal "Kane's Blog", Bunko.configuration.post_types.first[:title]
end

test "post_type accepts title with ampersand" do
Bunko.configure do |config|
config.post_type "news", title: "News & Updates"
end

assert_equal "News & Updates", Bunko.configuration.post_types.first[:title]
end

test "post_type accepts title with unicode characters" do
Bunko.configure do |config|
config.post_type "blog", title: "Café Notes — 日本語ブログ"
end

assert_equal "Café Notes — 日本語ブログ", Bunko.configuration.post_types.first[:title]
end

test "post_type accepts title with typographic quotes" do
Bunko.configure do |config|
config.post_type "blog" do |type|
type.title = "The “Best” Blog"
end
end

assert_equal "The “Best” Blog", Bunko.configuration.post_types.first[:title]
end

test "post_type accepts title with percent and angle brackets when not ERB delimiters" do
Bunko.configure do |config|
config.post_type "deals", title: "Deals < 50% off >"
end

assert_equal "Deals < 50% off >", Bunko.configuration.post_types.first[:title]
end

test "collection accepts title with apostrophe and ampersand" do
Bunko.configure do |config|
config.post_type "articles"
config.collection "resources", title: "Editor's Picks & Favorites", post_types: ["articles"]
end

assert_equal "Editor's Picks & Favorites", Bunko.configuration.collections.first[:title]
end

test "collection accepts title with unicode set via block" do
Bunko.configure do |config|
config.post_type "articles"
config.collection "resources" do |c|
c.title = "Ressourcen für Anfänger"
c.post_types = ["articles"]
end
end

assert_equal "Ressourcen für Anfänger", Bunko.configuration.collections.first[:title]
end

test "auto-generated titles remain valid" do
Bunko.configure do |config|
config.post_type "case_studies"
config.collection "long_reads", post_types: ["case_studies"]
end

assert_equal "Case Studies", Bunko.configuration.post_types.first[:title]
assert_equal "Long Reads", Bunko.configuration.collections.first[:title]
end
end
Loading