diff --git a/lib/bunko/configuration.rb b/lib/bunko/configuration.rb index 2b4cb63..e84ff0d 100644 --- a/lib/bunko/configuration.rb +++ b/lib/bunko/configuration.rb @@ -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 @@ -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" @@ -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 diff --git a/test/configuration/title_validation_test.rb b/test/configuration/title_validation_test.rb new file mode 100644 index 0000000..e7ff0a6 --- /dev/null +++ b/test/configuration/title_validation_test.rb @@ -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