From ce240d8bdf66cc050f38086a410dcc980498c9c1 Mon Sep 17 00:00:00 2001 From: Mike Dalessio Date: Mon, 10 Jan 2022 22:44:46 -0500 Subject: [PATCH] fix: regression in XSLT.quote_params handling non-strings Along the way, rewrite this method, refactor the tests, and improve the documentation. Closes #2418 --- ext/nokogiri/xslt_stylesheet.c | 116 ++++++++++++++++++++++++++++++--- lib/nokogiri/xslt.rb | 31 +++++---- test/test_xslt_transforms.rb | 81 ++++++++++++++++------- 3 files changed, 182 insertions(+), 46 deletions(-) diff --git a/ext/nokogiri/xslt_stylesheet.c b/ext/nokogiri/xslt_stylesheet.c index 02247a3e34..0b75886d38 100644 --- a/ext/nokogiri/xslt_stylesheet.c +++ b/ext/nokogiri/xslt_stylesheet.c @@ -107,19 +107,117 @@ serialize(VALUE self, VALUE xmlobj) } /* - * call-seq: - * transform(document, params = []) + * call-seq: + * transform(document) + * transform(document, params = {}) + * + * Apply an XSLT stylesheet to an XML::Document. + * + * [Parameters] + * - +document+ (Nokogiri::XML::Document) the document to be transformed. + * - +params+ (Hash, Array) strings used as XSLT parameters. + * + * [Returns] Nokogiri::XML::Document + * + * *Example* of basic transformation: + * + * xslt = <<~XSLT + * + * + * + * + * + * + * + *

+ *
    + * + *
  1. + *
    + *
+ * + * + *
+ * XSLT + * + * xml = <<~XML + * + * + * + * EMP0001 + * Accountant + * + * + * EMP0002 + * Developer + * + * + * XML + * + * doc = Nokogiri::XML::Document.parse(xml) + * stylesheet = Nokogiri::XSLT.parse(xslt) + * + * ⚠ Note that the +h1+ element is empty because no param has been provided! + * + * stylesheet.transform(doc).to_xml + * # => "\n" + + * # "

\n" + + * # "
    \n" + + * # "
  1. EMP0001
  2. \n" + + * # "
  3. EMP0002
  4. \n" + + * # "
\n" + + * # "\n" + * + * *Example* of using an input parameter hash: + * + * ⚠ The title is populated, but note how we need to quote-escape the value. + * + * stylesheet.transform(doc, { "title" => "'Employee List'" }).to_xml + * # => "\n" + + * # "

Employee List

\n" + + * # "
    \n" + + * # "
  1. EMP0001
  2. \n" + + * # "
  3. EMP0002
  4. \n" + + * # "
\n" + + * # "\n" + * + * *Example* using the XSLT.quote_params helper method to safely quote-escape strings: + * + * stylesheet.transform(doc, Nokogiri::XSLT.quote_params({ "title" => "Aaron's List" })).to_xml + * # => "\n" + + * # "

Aaron's List

\n" + + * # "
    \n" + + * # "
  1. EMP0001
  2. \n" + + * # "
  3. EMP0002
  4. \n" + + * # "
\n" + + * # "\n" + * + * *Example* using an array of XSLT parameters + * + * You can also use an array if you want to. * - * Apply an XSLT stylesheet to an XML::Document. - * +params+ is an array of strings used as XSLT parameters. - * returns Nokogiri::XML::Document + * stylesheet.transform(doc, ["title", "'Employee List'"]).to_xml + * # => "\n" + + * # "

Employee List

\n" + + * # "
    \n" + + * # "
  1. EMP0001
  2. \n" + + * # "
  3. EMP0002
  4. \n" + + * # "
\n" + + * # "\n" * - * Example: + * Or pass an array to XSLT.quote_params: * - * doc = Nokogiri::XML(File.read(ARGV[0])) - * xslt = Nokogiri::XSLT(File.read(ARGV[1])) - * puts xslt.transform(doc, ['key', 'value']) + * stylesheet.transform(doc, Nokogiri::XSLT.quote_params(["title", "Aaron's List"])).to_xml + * # => "\n" + + * # "

Aaron's List

\n" + + * # "
    \n" + + * # "
  1. EMP0001
  2. \n" + + * # "
  3. EMP0002
  4. \n" + + * # "
\n" + + * # "\n" * + * See: Nokogiri::XSLT.quote_params */ static VALUE transform(int argc, VALUE *argv, VALUE self) diff --git a/lib/nokogiri/xslt.rb b/lib/nokogiri/xslt.rb index 487455a55e..77d8ffaa26 100644 --- a/lib/nokogiri/xslt.rb +++ b/lib/nokogiri/xslt.rb @@ -1,3 +1,4 @@ +# coding: utf-8 # frozen_string_literal: true module Nokogiri @@ -34,22 +35,28 @@ def parse(string, modules = {}) end end - ### - # Quote parameters in +params+ for stylesheet safety + # :call-seq: + # quote_params(params) → Array + # + # Quote parameters in +params+ for stylesheet safety. + # See Nokogiri::XSLT::Stylesheet.transform for example usage. + # + # [Parameters] + # - +params+ (Hash, Array) XSLT parameters (key->value, or tuples of [key, value]) + # + # [Returns] Array of string parameters, with quotes correctly escaped for use with XSLT::Stylesheet.transform + # def quote_params(params) - parray = (params.instance_of?(Hash) ? params.to_a.flatten : params).dup - parray.each_with_index do |v, i| - parray[i] = if i % 2 > 0 - if /'/.match?(v) - "concat('#{v.gsub(/'/, %q{', "'", '})}')" - else - "'#{v}'" - end + params.flatten.each_slice(2).each_with_object([]) do |kv, quoted_params| + key, value = kv.map(&:to_s) + value = if /'/.match?(value) + "concat('#{value.gsub(/'/, %q{', "'", '})}')" else - v.to_s + "'#{value}'" end + quoted_params << key + quoted_params << value end - parray.flatten end end end diff --git a/test/test_xslt_transforms.rb b/test/test_xslt_transforms.rb index 048f915bbb..068498484f 100644 --- a/test/test_xslt_transforms.rb +++ b/test/test_xslt_transforms.rb @@ -4,6 +4,12 @@ class Nokogiri::TestCase describe Nokogiri::XSLT::Stylesheet do + def check_params(result_doc, params) + result_doc.xpath("/root/params/*").each do |p| + assert_equal(p.content, params[p.name.intern]) + end + end + let(:doc) { Nokogiri::XML(File.open(XML_FILE)) } def test_class_methods @@ -180,25 +186,6 @@ def test_transform_with_quote_params assert_equal("Booyah", result_doc.at_css("h1").content) end - def test_quote_params - h = { - :sym => %{xxx}, - "str" => %{"xxx"}, - :sym2 => %{'xxx'}, - "str2" => %{x'x'x}, - :sym3 => %{x"x"x}, - } - hh = h.dup - result_hash = Nokogiri::XSLT.quote_params(h) - assert_equal(hh, h) # non-destructive - - a = h.to_a.flatten - result_array = Nokogiri::XSLT.quote_params(a) - assert_equal(h.to_a.flatten, a) # non-destructive - - assert_equal(result_array, result_hash) - end - def test_exslt # see http://yokolet.blogspot.com/2010/10/pure-java-nokogiri-xslt-extension.html") skip_unless_libxml2("cannot get it working on JRuby") @@ -292,12 +279,6 @@ def test_passing_a_non_document_to_transform assert_raises(ArgumentError) { xsl.transform(Nokogiri::HTML("").css("body")) } end - def check_params(result_doc, params) - result_doc.xpath("/root/params/*").each do |p| - assert_equal(p.content, params[p.name.intern]) - end - end - def test_non_html_xslt_transform xml = Nokogiri.XML(<<~EOXML) @@ -402,5 +383,55 @@ def test_non_html_xslt_transform assert_equal("<>", result.children.to_xml) end end + + describe "XSLT.quote_params" do + it "returns quoted values" do + assert_equal(["asdf", "'qwer'"], Nokogiri::XSLT.quote_params({ "asdf" => "qwer" })) + end + + it "stringifies non-string keys and values" do + assert_equal(["asdf", "'1234'"], Nokogiri::XSLT.quote_params({ asdf: 1234 })) + assert_equal(["1234", "'asdf'"], Nokogiri::XSLT.quote_params({ 1234 => :asdf })) + end + + it "handles multiple key-value pairs" do + actual = Nokogiri::XSLT.quote_params({ "abcd" => "efgh", "ijkl" => "mnop" }) + expected = ["abcd", "'efgh'", "ijkl", "'mnop'"] + assert_equal(expected, actual) + end + + it "handles an array of pairs" do + actual = Nokogiri::XSLT.quote_params(["abcd", "efgh", "ijkl", "mnop"]) + expected = ["abcd", "'efgh'", "ijkl", "'mnop'"] + assert_equal(expected, actual) + end + + it "handles double quotes" do + assert_equal(["a", %{'"asdf"'}], Nokogiri::XSLT.quote_params({ "a" => %{"asdf"} })) + end + + it "handles single quotes" do + actual = Nokogiri::XSLT.quote_params({ "a" => %{'asdf'} }) + expected = ["a", %{concat('', "'", 'asdf', "'", '')}] + assert_equal(expected, actual) + + actual = Nokogiri::XSLT.quote_params({ "a" => %{a'sd'f} }) + expected = ["a", %{concat('a', "'", 'sd', "'", 'f')}] + assert_equal(expected, actual) + end + + it "does not change the input parameters" do + input_h = { "abcd" => "efgh", "ijkl" => "mnop" } + expected_h = input_h.dup + input_a = input_h.to_a.flatten + expected_a = input_a.dup + + Nokogiri::XSLT.quote_params(input_h) + assert_equal(expected_h, input_h) + + Nokogiri::XSLT.quote_params(input_a) + assert_equal(expected_a, input_a) + end + end end end