Skip to content

Commit

Permalink
fix: regression in XSLT.quote_params handling non-strings
Browse files Browse the repository at this point in the history
Along the way, rewrite this method, refactor the tests, and improve
the documentation.

Closes #2418
  • Loading branch information
flavorjones committed Jan 11, 2022
1 parent be17829 commit ce240d8
Show file tree
Hide file tree
Showing 3 changed files with 182 additions and 46 deletions.
116 changes: 107 additions & 9 deletions ext/nokogiri/xslt_stylesheet.c
Expand Up @@ -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
* <xsl:stylesheet version="1.0"
* xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
*
* <xsl:param name="title"/>
*
* <xsl:template match="/">
* <html>
* <body>
* <h1><xsl:value-of select="$title"/></h1>
* <ol>
* <xsl:for-each select="staff/employee">
* <li><xsl:value-of select="employeeId"></li>
* </xsl:for-each>
* </ol>
* </body>
* </html>
* </xsl:stylesheet>
* XSLT
*
* xml = <<~XML
* <?xml version="1.0"?>
* <staff>
* <employee>
* <employeeId>EMP0001</employeeId>
* <position>Accountant</position>
* </employee>
* <employee>
* <employeeId>EMP0002</employeeId>
* <position>Developer</position>
* </employee>
* </staff>
* 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
* # => "<html><body>\n" +
* # "<h1></h1>\n" +
* # "<ol>\n" +
* # "<li>EMP0001</li>\n" +
* # "<li>EMP0002</li>\n" +
* # "</ol>\n" +
* # "</body></html>\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
* # => "<html><body>\n" +
* # "<h1>Employee List</h1>\n" +
* # "<ol>\n" +
* # "<li>EMP0001</li>\n" +
* # "<li>EMP0002</li>\n" +
* # "</ol>\n" +
* # "</body></html>\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
* # => "<html><body>\n" +
* # "<h1>Aaron's List</h1>\n" +
* # "<ol>\n" +
* # "<li>EMP0001</li>\n" +
* # "<li>EMP0002</li>\n" +
* # "</ol>\n" +
* # "</body></html>\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
* # => "<html><body>\n" +
* # "<h1>Employee List</h1>\n" +
* # "<ol>\n" +
* # "<li>EMP0001</li>\n" +
* # "<li>EMP0002</li>\n" +
* # "</ol>\n" +
* # "</body></html>\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
* # => "<html><body>\n" +
* # "<h1>Aaron's List</h1>\n" +
* # "<ol>\n" +
* # "<li>EMP0001</li>\n" +
* # "<li>EMP0002</li>\n" +
* # "</ol>\n" +
* # "</body></html>\n"
*
* See: Nokogiri::XSLT.quote_params
*/
static VALUE
transform(int argc, VALUE *argv, VALUE self)
Expand Down
31 changes: 19 additions & 12 deletions lib/nokogiri/xslt.rb
@@ -1,3 +1,4 @@
# coding: utf-8
# frozen_string_literal: true

module Nokogiri
Expand Down Expand Up @@ -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
Expand Down
81 changes: 56 additions & 25 deletions test/test_xslt_transforms.rb
Expand Up @@ -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
Expand Down Expand Up @@ -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")
Expand Down Expand Up @@ -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)
<a>
Expand Down Expand Up @@ -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

0 comments on commit ce240d8

Please sign in to comment.