diff --git a/app/logical/tag_name_validator.rb b/app/logical/tag_name_validator.rb index 491737dce..3a1341316 100644 --- a/app/logical/tag_name_validator.rb +++ b/app/logical/tag_name_validator.rb @@ -18,6 +18,10 @@ class TagNameValidator < ActiveModel::EachValidator record.errors.add(attribute, "'#{value}' cannot be more than #{MAX_TAG_LENGTH} characters long") end + if !value.in?(Tag::PERMITTED_UNBALANCED_TAGS) && !value.has_balanced_parens? + record.errors.add(attribute, "'#{value}' cannot have unbalanced parentheses") + end + case value when /\A_*\z/ record.errors.add(attribute, "cannot be blank") diff --git a/app/models/tag.rb b/app/models/tag.rb index 373d69e45..59fdb11fd 100644 --- a/app/models/tag.rb +++ b/app/models/tag.rb @@ -3,6 +3,9 @@ class Tag < ApplicationRecord ABBREVIATION_REGEXP = /([a-z0-9])[a-z0-9']*($|[^a-z0-9']+)/ + # Tags that are permitted to have unbalanced parentheses, as a special exception to the normal rule that parentheses in tags must balanced. + PERMITTED_UNBALANCED_TAGS = %w[:) :( ;) ;( >:) >:(] + has_one :wiki_page, :foreign_key => "title", :primary_key => "name" has_one :artist, :foreign_key => "name", :primary_key => "name" has_one :antecedent_alias, -> {active}, :class_name => "TagAlias", :foreign_key => "antecedent_name", :primary_key => "name" diff --git a/config/initializers/core_extensions.rb b/config/initializers/core_extensions.rb index ef828ddde..c98797b73 100644 --- a/config/initializers/core_extensions.rb +++ b/config/initializers/core_extensions.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module Danbooru module Extensions module String @@ -55,6 +57,22 @@ module Danbooru text end + + # @return [Boolean] True if the string contains only balanced parentheses; false if the string contains unbalanced parentheses. + def has_balanced_parens?(open = "(", close = ")") + parens = 0 + + chars.each do |char| + if char == open + parens += 1 + elsif char == close + parens -= 1 + return false if parens < 0 + end + end + + parens == 0 + end end end end diff --git a/test/unit/tag_test.rb b/test/unit/tag_test.rb index 03d4e3518..9aea0f416 100644 --- a/test/unit/tag_test.rb +++ b/test/unit/tag_test.rb @@ -147,6 +147,16 @@ class TagTest < ActiveSupport::TestCase should allow_value("foo bar").for(:name).on(:create) should allow_value("FOO").for(:name).on(:create) + should allow_value(":)").for(:name).on(:create) + should allow_value(":(").for(:name).on(:create) + should allow_value(";)").for(:name).on(:create) + should allow_value(";(").for(:name).on(:create) + should allow_value(">:)").for(:name).on(:create) + should allow_value(">:(").for(:name).on(:create) + + should allow_value("foo_(bar)").for(:name).on(:create) + should allow_value("foo_(bar_(baz))").for(:name).on(:create) + should_not allow_value("").for(:name).on(:create) should_not allow_value("___").for(:name).on(:create) should_not allow_value("~foo").for(:name).on(:create) @@ -154,6 +164,7 @@ class TagTest < ActiveSupport::TestCase should_not allow_value("/foo").for(:name).on(:create) should_not allow_value("`foo").for(:name).on(:create) should_not allow_value("%foo").for(:name).on(:create) + should_not allow_value("(foo").for(:name).on(:create) should_not allow_value(")foo").for(:name).on(:create) should_not allow_value("{foo").for(:name).on(:create) should_not allow_value("}foo").for(:name).on(:create) @@ -169,6 +180,12 @@ class TagTest < ActiveSupport::TestCase should_not allow_value("FAV:blah").for(:name).on(:create) should_not allow_value("X"*171).for(:name).on(:create) + should_not allow_value("foo)").for(:name).on(:create) + should_not allow_value("foo(").for(:name).on(:create) + should_not allow_value("foo)(").for(:name).on(:create) + should_not allow_value("foo(()").for(:name).on(:create) + should_not allow_value("foo())").for(:name).on(:create) + metatags = PostQueryBuilder::METATAGS + TagCategory.mapping.keys metatags.each do |metatag| should_not allow_value("#{metatag}:foo").for(:name).on(:create)