# encoding: utf-8
require_relative 'common'

describe 'Sanitize::CSS' do
  make_my_diffs_pretty!
  parallelize_me!

  describe 'instance methods' do
    before do
      @default = Sanitize::CSS.new
      @relaxed = Sanitize::CSS.new(Sanitize::Config::RELAXED[:css])
      @custom  = Sanitize::CSS.new(:properties => %w[background color width])
    end

    describe '#properties' do
      it 'should sanitize CSS properties' do
        css = 'background: #fff; width: expression(alert("hi"));'

        _(@default.properties(css)).must_equal ' '
        _(@relaxed.properties(css)).must_equal 'background: #fff; '
        _(@custom.properties(css)).must_equal 'background: #fff; '
      end

      it 'should allow allowlisted URL protocols' do
        [
          "background: url(relative.jpg)",
          "background: url('relative.jpg')",
          "background: url(http://example.com/http.jpg)",
          "background: url('ht\\tp://example.com/http.jpg')",
          "background: url(https://example.com/https.jpg)",
          "background: url('https://example.com/https.jpg')",
          "background: image-set('relative.jpg' 1x, 'relative-2x.jpg' 2x)",
          "background: image-set('https://example.com/https.jpg' 1x, 'https://example.com/https-2x.jpg' 2x)",
          "background: image-set('https://example.com/https.jpg' type('image/jpeg'), 'https://example.com/https.avif' type('image/avif'))",
          "background: -webkit-image-set('relative.jpg' 1x, 'relative-2x.jpg' 2x)",
          "background: -webkit-image-set('https://example.com/https.jpg' 1x, 'https://example.com/https-2x.jpg' 2x)",
          "background: -webkit-image-set('https://example.com/https.jpg' type('image/jpeg'), 'https://example.com/https.avif' type('image/avif'))",
          "background: image('relative.jpg');",
          "background: image('https://example.com/https.jpg');",
          "background: image(rtl 'https://example.com/https.jpg');"
        ].each do |css|
          _(@default.properties(css)).must_equal ''
          _(@relaxed.properties(css)).must_equal css
          _(@custom.properties(css)).must_equal ''
        end
      end

      it 'should not allow non-allowlisted URL protocols' do
        [
          "background: url(javascript:alert(0))",
          "background: url(ja\\56 ascript:alert(0))",
          "background: url('javascript:foo')",
          "background: url('ja\\56 ascript:alert(0)')",
          "background: url('ja\\va\\script\\:alert(0)')",
          "background: url('javas\\\ncript:alert(0)')",
          "background: url('java\\0script:foo')"
        ].each do |css|
          _(@default.properties(css)).must_equal ''
          _(@relaxed.properties(css)).must_equal ''
          _(@custom.properties(css)).must_equal ''
        end
      end

      it 'should not allow -moz-binding' do
        css = "-moz-binding:url('http://ha.ckers.org/xssmoz.xml#xss')"

        _(@default.properties(css)).must_equal ''
        _(@relaxed.properties(css)).must_equal ''
        _(@custom.properties(css)).must_equal ''
      end

      it 'should not allow expressions' do
        [
          "width:expression(alert(1))",
          "width:  /**/expression(alert(1)",
          "width:e\\78 pression(\n\nalert(\n1)",
          "width:\nexpression(alert(1));",
          "xss:expression(alert(1))",
          "height: foo(expression(alert(1)));"
        ].each do |css|
          _(@default.properties(css)).must_equal ''
          _(@relaxed.properties(css)).must_equal ''
          _(@custom.properties(css)).must_equal ''
        end
      end

      it 'should not allow behaviors' do
        css = "behavior: url(xss.htc);"

        _(@default.properties(css)).must_equal ''
        _(@relaxed.properties(css)).must_equal ''
        _(@custom.properties(css)).must_equal ''
      end

      describe 'when :allow_comments is true' do
        it 'should preserve comments' do
          _(@relaxed.properties('color: #fff; /* comment */ width: 100px;'))
            .must_equal 'color: #fff; /* comment */ width: 100px;'

          _(@relaxed.properties("color: #fff; /* \n\ncomment */ width: 100px;"))
            .must_equal "color: #fff; /* \n\ncomment */ width: 100px;"
        end
      end

      describe 'when :allow_comments is false' do
        it 'should strip comments' do
          _(@custom.properties('color: #fff; /* comment */ width: 100px;'))
            .must_equal 'color: #fff;  width: 100px;'

          _(@custom.properties("color: #fff; /* \n\ncomment */ width: 100px;"))
            .must_equal 'color: #fff;  width: 100px;'
        end
      end

      describe 'when :allow_hacks is true' do
        it 'should allow common CSS hacks' do
          _(@relaxed.properties('_border: 1px solid #fff; *width: 10px'))
            .must_equal '_border: 1px solid #fff; *width: 10px'
        end
      end

      describe 'when :allow_hacks is false' do
        it 'should not allow common CSS hacks' do
          _(@custom.properties('_border: 1px solid #fff; *width: 10px'))
            .must_equal ' '
        end
      end
    end

    describe '#stylesheet' do
      it 'should sanitize a CSS stylesheet' do
        css = %[
          /* Yay CSS! */
          .foo { color: #fff; }
          #bar { background: url(yay.jpg); }

          @media screen (max-width:480px) {
            .foo { width: 400px; }
            #bar:not(.baz) { height: 100px; }
          }
        ].strip

        _(@default.stylesheet(css).strip).must_equal %[
          .foo {  }
          #bar {  }
        ].strip

        _(@relaxed.stylesheet(css)).must_equal css

        _(@custom.stylesheet(css).strip).must_equal %[
          .foo { color: #fff; }
          #bar {  }
        ].strip
      end

      describe 'when :allow_comments is true' do
        it 'should preserve comments' do
          _(@relaxed.stylesheet('.foo { color: #fff; /* comment */ width: 100px; }'))
            .must_equal '.foo { color: #fff; /* comment */ width: 100px; }'

          _(@relaxed.stylesheet(".foo { color: #fff; /* \n\ncomment */ width: 100px; }"))
            .must_equal ".foo { color: #fff; /* \n\ncomment */ width: 100px; }"
        end
      end

      describe 'when :allow_comments is false' do
        it 'should strip comments' do
          _(@custom.stylesheet('.foo { color: #fff; /* comment */ width: 100px; }'))
            .must_equal '.foo { color: #fff;  width: 100px; }'

          _(@custom.stylesheet(".foo { color: #fff; /* \n\ncomment */ width: 100px; }"))
            .must_equal '.foo { color: #fff;  width: 100px; }'
        end
      end

      describe 'when :allow_hacks is true' do
        it 'should allow common CSS hacks' do
          _(@relaxed.stylesheet('.foo { _border: 1px solid #fff; *width: 10px }'))
            .must_equal '.foo { _border: 1px solid #fff; *width: 10px }'
        end
      end

      describe 'when :allow_hacks is false' do
        it 'should not allow common CSS hacks' do
          _(@custom.stylesheet('.foo { _border: 1px solid #fff; *width: 10px }'))
            .must_equal '.foo {  }'
        end
      end
    end

    describe '#tree!' do
      it 'should sanitize a Crass CSS parse tree' do
        tree = Crass.parse(String.new("@import url(foo.css);\n") <<
          ".foo { background: #fff; font: 16pt 'Comic Sans MS'; }\n" <<
          "#bar { top: 125px; background: green; }")

        _(@custom.tree!(tree)).must_be_same_as tree

        _(Crass::Parser.stringify(tree)).must_equal String.new("\n") <<
            ".foo { background: #fff;  }\n" <<
            "#bar {  background: green; }"
      end
    end
  end

  describe 'class methods' do
    describe '.properties' do
      it 'should sanitize CSS properties with the given config' do
        css = 'background: #fff; width: expression(alert("hi"));'

        _(Sanitize::CSS.properties(css)).must_equal ' '
        _(Sanitize::CSS.properties(css, Sanitize::Config::RELAXED[:css])).must_equal 'background: #fff; '
        _(Sanitize::CSS.properties(css, :properties => %w[background color width])).must_equal 'background: #fff; '
      end
    end

    describe '.stylesheet' do
      it 'should sanitize a CSS stylesheet with the given config' do
        css = %[
          /* Yay CSS! */
          .foo { color: #fff; }
          #bar { background: url(yay.jpg); }

          @media screen (max-width:480px) {
            .foo { width: 400px; }
            #bar:not(.baz) { height: 100px; }
          }
        ].strip

        _(Sanitize::CSS.stylesheet(css).strip).must_equal %[
          .foo {  }
          #bar {  }
        ].strip

        _(Sanitize::CSS.stylesheet(css, Sanitize::Config::RELAXED[:css])).must_equal css

        _(Sanitize::CSS.stylesheet(css, :properties => %w[background color width]).strip).must_equal %[
          .foo { color: #fff; }
          #bar {  }
        ].strip
      end
    end

    describe '.tree!' do
      it 'should sanitize a Crass CSS parse tree with the given config' do
        tree = Crass.parse(String.new("@import url(foo.css);\n") <<
          ".foo { background: #fff; font: 16pt 'Comic Sans MS'; }\n" <<
          "#bar { top: 125px; background: green; }")

        _(Sanitize::CSS.tree!(tree, :properties => %w[background color width])).must_be_same_as tree

        _(Crass::Parser.stringify(tree)).must_equal String.new("\n") <<
            ".foo { background: #fff;  }\n" <<
            "#bar {  background: green; }"
      end
    end
  end

  describe 'functionality' do
    before do
      @default = Sanitize::CSS.new
      @relaxed = Sanitize::CSS.new(Sanitize::Config::RELAXED[:css])
    end

    # https://github.com/rgrove/sanitize/issues/121
    it 'should parse the contents of @media rules properly' do
      css = '@media { p[class="center"] { text-align: center; }}'
      _(@relaxed.stylesheet(css)).must_equal css

      css = %[
        @media (max-width: 720px) {
          p.foo > .bar { float: right; width: expression(body.scrollLeft + 50 + 'px'); }
          #baz { color: green; }

          @media (orientation: portrait) {
            #baz { color: red; }
          }
        }
      ].strip

      _(@relaxed.stylesheet(css)).must_equal %[
        @media (max-width: 720px) {
          p.foo > .bar { float: right;  }
          #baz { color: green; }

          @media (orientation: portrait) {
            #baz { color: red; }
          }
        }
      ].strip
    end

    it 'should parse @page rules properly' do
      css = %[
        @page { margin: 2cm } /* All margins set to 2cm */

        @page :right {
          @top-center { content: "Preliminary edition" }
          @bottom-center { content: counter(page) }
        }

        @page {
          size: 8.5in 11in;
          margin: 10%;

          @top-left {
            content: "Hamlet";
          }
          @top-right {
            content: "Page " counter(page);
          }
        }
      ].strip

      _(@relaxed.stylesheet(css)).must_equal css
    end

    describe ":at_rules" do
      it "should remove blockless at-rules that aren't allowlisted" do
        css = %[
          @charset 'utf-8';
          @import url('foo.css');
          .foo { color: green; }
        ].strip

        _(@relaxed.stylesheet(css).strip).must_equal %[
          .foo { color: green; }
        ].strip
      end

      describe "when blockless at-rules are allowlisted" do
        before do
          @scss = Sanitize::CSS.new(Sanitize::Config.merge(Sanitize::Config::RELAXED[:css], {
            :at_rules => ['charset', 'import']
          }))
        end

        it "should not remove them" do
          css = %[
            @charset 'utf-8';
            @import url('foo.css');
            .foo { color: green; }
          ].strip

          _(@scss.stylesheet(css)).must_equal %[
            @charset 'utf-8';
            @import url('foo.css');
            .foo { color: green; }
          ].strip
        end

        it "should remove them if they have invalid blocks" do
          css = %[
            @charset { color: green }
            @import { color: green }
            .foo { color: green; }
          ].strip

          _(@scss.stylesheet(css).strip).must_equal %[
            .foo { color: green; }
          ].strip
        end
      end

      describe "when validating @import rules" do

        describe "with no validation proc specified" do
          before do
            @scss = Sanitize::CSS.new(Sanitize::Config.merge(Sanitize::Config::RELAXED[:css], {
              :at_rules => ['import']
            }))
          end

          it "should allow any URL value" do
            css = %[
              @import url('https://somesite.com/something.css');
            ].strip

            _(@scss.stylesheet(css).strip).must_equal %[
              @import url('https://somesite.com/something.css');
            ].strip
          end
        end

        describe "with a validation proc specified" do
          before do
            google_font_validator = Proc.new { |url| url.start_with?("https://fonts.googleapis.com") }

            @scss = Sanitize::CSS.new(Sanitize::Config.merge(Sanitize::Config::RELAXED[:css], {
              :at_rules => ['import'], :import_url_validator => google_font_validator
            }))
          end

          it "should allow a google fonts url" do
            css = %[
              @import 'https://fonts.googleapis.com/css?family=Indie+Flower';
              @import url('https://fonts.googleapis.com/css?family=Indie+Flower');
            ].strip

            _(@scss.stylesheet(css).strip).must_equal %[
              @import 'https://fonts.googleapis.com/css?family=Indie+Flower';
              @import url('https://fonts.googleapis.com/css?family=Indie+Flower');
            ].strip
          end

          it "should not allow a nasty url" do
            css = %[
              @import 'https://fonts.googleapis.com/css?family=Indie+Flower';
              @import 'https://nastysite.com/nasty_hax0r.css';
              @import url('https://nastysite.com/nasty_hax0r.css');
            ].strip

            _(@scss.stylesheet(css).strip).must_equal %[
              @import 'https://fonts.googleapis.com/css?family=Indie+Flower';
            ].strip
          end

          it "should not allow a blank url" do
            css = %[
              @import 'https://fonts.googleapis.com/css?family=Indie+Flower';
              @import '';
              @import url('');
            ].strip

            _(@scss.stylesheet(css).strip).must_equal %[
              @import 'https://fonts.googleapis.com/css?family=Indie+Flower';
            ].strip
          end
        end
      end
    end
  end
end
