Skip to content

Commit 4d5754b

Browse files
committed
backport #2320
commit cb2b9cfae3bc335beea7300f813a7f3e0b2776f1 Author: Mike Dalessio <mike.dalessio@gmail.com> Date: Wed Aug 18 20:12:41 2021 -0400 update CHANGELOG commit c5074b51f26f9b72bacf11c65f16d637440d93fc Author: Mike Dalessio <mike.dalessio@gmail.com> Date: Sat Aug 28 15:19:02 2021 -0400 feat/fix: re-enable namespace_inheritance for Builder documents This restores the Builder behavior from Nokogiri versions before 1.12.0. This can be switched off by passing a kwarg to the Builder initializer. See #2317. commit 3814b47d4c3503cf4960caa2d0c5af84b982ee80 Author: Mike Dalessio <mike.dalessio@gmail.com> Date: Sat Aug 28 15:15:07 2021 -0400 feat/fix: introduce Document#namespace_inheritance attr When true, reparented elements without a namespace will inherit their new parent's namespace (if one exists). Defaults to +false+. This is part of addressing the behavior change introduced in v1.12 by 1f483f0, allowing us to switch it on or off per-document. See #2317.
1 parent e3f2209 commit 4d5754b

File tree

9 files changed

+227
-29
lines changed

9 files changed

+227
-29
lines changed

CHANGELOG.md

Lines changed: 55 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,61 @@ Nokogiri follows [Semantic Versioning](https://semver.org/), please see the [REA
44

55
---
66

7-
## next / unreleased
7+
## 1.12.4 / unreleased
8+
9+
### Notable fix: Namespace inheritance
10+
11+
Namespace behavior when reparenting nodes has historically been poorly specified and the behavior
12+
diverged between CRuby and JRuby. As a result, making this behavior consistent in v1.12.0 introduced
13+
a breaking change.
14+
15+
This patch release reverts the Builder behavior present in v1.12.0..v1.12.3 but keeps the Document
16+
behavior. This release also introduces a Document attribute to allow affected users to easily change
17+
this behavior for their legacy code without invasive changes.
18+
19+
20+
#### Compensating Feature in XML::Document
21+
22+
This release of Nokogiri introduces a new `Document` boolean attribute, `namespace_inheritance`,
23+
which controls whether children should inherit a namespace when they are reparented.
24+
`Nokogiri::XML:Document` defaults this attribute to `false` meaning "do not inherit," thereby making
25+
explicit the behavior change introduced in v1.12.0.
26+
27+
CRuby users who desire the pre-v1.12.0 behavior may set `document.namespace_inheritance = true` before
28+
reparenting nodes.
29+
30+
See https://nokogiri.org/rdoc/Nokogiri/XML/Document.html#namespace_inheritance-instance_method for
31+
example usage.
32+
33+
34+
#### Fix for XML::Builder
35+
36+
However, recognizing that we want `Builder`-created children to inherit namespaces, Builder now will
37+
set `namespace_inheritance=true` on the underlying document for both JRuby and CRuby. This means that, on CRuby, the pre-v1.12.0 behavior is restored.
38+
39+
Users who want to turn this behavior off may pass a keyword argument to the Builder constructor like
40+
so:
41+
42+
``` ruby
43+
Nokogiri::XML::Builder.new(namespace_inheritance: false)
44+
```
45+
46+
See https://nokogiri.org/rdoc/Nokogiri/XML/Builder.html#label-Namespace+inheritance for example
47+
usage.
48+
49+
50+
#### Downstream gem maintainers
51+
52+
Note that any downstream gems may want to specifically omit Nokogiri v1.12.0--v1.12.3 from their dependency specification if they rely on child namespace inheritance:
53+
54+
``` ruby
55+
Gem::Specification.new do |gem|
56+
# ...
57+
gem.add_runtime_dependency 'nokogiri', '!=1.12.3', '!=1.12.2', '!=1.12.1', '!=1.12.0'
58+
# ...
59+
end
60+
```
61+
862

963
### Fixed
1064

ext/java/nokogiri/XmlDocumentFragment.java

Lines changed: 0 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -34,8 +34,6 @@
3434
public class XmlDocumentFragment extends XmlNode
3535
{
3636

37-
private XmlElement fragmentContext;
38-
3937
public
4038
XmlDocumentFragment(Ruby ruby)
4139
{
@@ -75,10 +73,6 @@ public class XmlDocumentFragment extends XmlNode
7573
fragment.setDocument(context, doc);
7674
fragment.setNode(context.runtime, doc.getDocument().createDocumentFragment());
7775

78-
//TODO: Get namespace definitions from doc.
79-
if (args.length == 3 && args[2] != null && args[2] instanceof XmlElement) {
80-
fragment.fragmentContext = (XmlElement)args[2];
81-
}
8276
Helpers.invoke(context, fragment, "initialize", args);
8377
return fragment;
8478
}
@@ -158,12 +152,6 @@ public class XmlDocumentFragment extends XmlNode
158152
return null;
159153
}
160154

161-
public XmlElement
162-
getFragmentContext()
163-
{
164-
return fragmentContext;
165-
}
166-
167155
@Override
168156
public void
169157
relink_namespace(ThreadContext context)

ext/java/nokogiri/XmlNode.java

Lines changed: 9 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
import org.apache.xerces.dom.CoreDocumentImpl;
1313
import org.jruby.Ruby;
1414
import org.jruby.RubyArray;
15+
import org.jruby.RubyBoolean;
1516
import org.jruby.RubyClass;
1617
import org.jruby.RubyFixnum;
1718
import org.jruby.RubyInteger;
@@ -421,7 +422,14 @@ public class XmlNode extends RubyObject
421422
String nsURI = e.lookupNamespaceURI(prefix);
422423
this.node = NokogiriHelpers.renameNode(e, nsURI, e.getNodeName());
423424

424-
if (nsURI == null || nsURI.isEmpty()) { return; }
425+
if (nsURI == null || nsURI.isEmpty()) {
426+
RubyBoolean ns_inherit =
427+
(RubyBoolean)document(context.runtime).getInstanceVariable("@namespace_inheritance");
428+
if (ns_inherit.isTrue()) {
429+
set_namespace(context, ((XmlNode)parent(context)).namespace(context));
430+
}
431+
return;
432+
}
425433

426434
String currentPrefix = e.getParentNode().lookupPrefix(nsURI);
427435
String currentURI = e.getParentNode().lookupNamespaceURI(prefix);
@@ -1743,24 +1751,11 @@ protected enum AdoptScheme {
17431751
e.appendChild(otherNode);
17441752
otherNode = e;
17451753
} else {
1746-
addNamespaceURIIfNeeded(otherNode);
17471754
parent.appendChild(otherNode);
17481755
}
17491756
return new Node[] { otherNode };
17501757
}
17511758

1752-
private void
1753-
addNamespaceURIIfNeeded(Node child)
1754-
{
1755-
if (this instanceof XmlDocumentFragment && ((XmlDocumentFragment) this).getFragmentContext() != null) {
1756-
XmlElement fragmentContext = ((XmlDocumentFragment) this).getFragmentContext();
1757-
String namespace_uri = fragmentContext.node.getNamespaceURI();
1758-
if (namespace_uri != null && namespace_uri.length() > 0) {
1759-
NokogiriHelpers.renameNode(child, namespace_uri, child.getNodeName());
1760-
}
1761-
}
1762-
}
1763-
17641759
protected void
17651760
adoptAsPrevSibling(ThreadContext context,
17661761
Node parent,

ext/nokogiri/xml_node.c

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,13 @@ relink_namespace(xmlNodePtr reparented)
6969
/* Avoid segv when relinking against unlinked nodes. */
7070
if (reparented->type != XML_ELEMENT_NODE || !reparented->parent) { return; }
7171

72+
/* Make sure that our reparented node has the correct namespaces */
73+
if (!reparented->ns &&
74+
(reparented->doc != (xmlDocPtr)reparented->parent) &&
75+
(rb_iv_get(DOC_RUBY_OBJECT(reparented->doc), "@namespace_inheritance") == Qtrue)) {
76+
xmlSetNs(reparented, reparented->parent->ns);
77+
}
78+
7279
/* Search our parents for an existing definition */
7380
if (reparented->nsDef) {
7481
xmlNsPtr curr = reparented->nsDef;

lib/nokogiri/xml/builder.rb

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -196,6 +196,41 @@ module XML
196196
#
197197
# Note the "foo:object" tag.
198198
#
199+
# === Namespace inheritance
200+
#
201+
# In the Builder context, children will inherit their parent's namespace. This is the same
202+
# behavior as if the underlying {XML::Document} set +namespace_inheritance+ to +true+:
203+
#
204+
# result = Nokogiri::XML::Builder.new do |xml|
205+
# xml["soapenv"].Envelope("xmlns:soapenv" => "http://schemas.xmlsoap.org/soap/envelope/") do
206+
# xml.Header
207+
# end
208+
# end
209+
# result.doc.to_xml
210+
# # => <?xml version="1.0" encoding="utf-8"?>
211+
# # <soapenv:Envelope xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/">
212+
# # <soapenv:Header/>
213+
# # </soapenv:Envelope>
214+
#
215+
# Users may turn this behavior off by passing a keyword argument +namespace_inheritance:false+
216+
# to the initializer:
217+
#
218+
# result = Nokogiri::XML::Builder.new(namespace_inheritance: false) do |xml|
219+
# xml["soapenv"].Envelope("xmlns:soapenv" => "http://schemas.xmlsoap.org/soap/envelope/") do
220+
# xml.Header
221+
# xml["soapenv"].Body # users may explicitly opt into the namespace
222+
# end
223+
# end
224+
# result.doc.to_xml
225+
# # => <?xml version="1.0" encoding="utf-8"?>
226+
# # <soapenv:Envelope xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/">
227+
# # <Header/>
228+
# # <soapenv:Body/>
229+
# # </soapenv:Envelope>
230+
#
231+
# For more information on namespace inheritance, please see {XML::Document#namespace_inheritance}
232+
#
233+
#
199234
# == Document Types
200235
#
201236
# To create a document type (DTD), access use the Builder#doc method to get
@@ -226,6 +261,8 @@ module XML
226261
# </root>
227262
#
228263
class Builder
264+
DEFAULT_DOCUMENT_OPTIONS = {namespace_inheritance: true}
265+
229266
# The current Document object being built
230267
attr_accessor :doc
231268

@@ -282,6 +319,7 @@ def initialize(options = {}, root = nil, &block)
282319
@arity = nil
283320
@ns = nil
284321

322+
options = DEFAULT_DOCUMENT_OPTIONS.merge(options)
285323
options.each do |k, v|
286324
@doc.send(:"#{k}=", v)
287325
end

lib/nokogiri/xml/document.rb

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -113,9 +113,55 @@ def self.parse string_or_io, url = nil, encoding = nil, options = ParseOptions::
113113
# A list of Nokogiri::XML::SyntaxError found when parsing a document
114114
attr_accessor :errors
115115

116+
# When true, reparented elements without a namespace will inherit their new parent's
117+
# namespace (if one exists). Defaults to +false+.
118+
#
119+
# @example Default behavior of namespace inheritance
120+
# xml = <<~EOF
121+
# <root xmlns:foo="http://nokogiri.org/default_ns/test/foo">
122+
# <foo:parent>
123+
# </foo:parent>
124+
# </root>
125+
# EOF
126+
# doc = Nokogiri::XML(xml)
127+
# parent = doc.at_xpath("//foo:parent", "foo" => "http://nokogiri.org/default_ns/test/foo")
128+
# parent.add_child("<child></child>")
129+
# doc.to_xml
130+
# # => <?xml version="1.0"?>
131+
# # <root xmlns:foo="http://nokogiri.org/default_ns/test/foo">
132+
# # <foo:parent>
133+
# # <child/>
134+
# # </foo:parent>
135+
# # </root>
136+
#
137+
# @example Setting namespace inheritance to +true+
138+
# xml = <<~EOF
139+
# <root xmlns:foo="http://nokogiri.org/default_ns/test/foo">
140+
# <foo:parent>
141+
# </foo:parent>
142+
# </root>
143+
# EOF
144+
# doc = Nokogiri::XML(xml)
145+
# doc.namespace_inheritance = true
146+
# parent = doc.at_xpath("//foo:parent", "foo" => "http://nokogiri.org/default_ns/test/foo")
147+
# parent.add_child("<child></child>")
148+
# doc.to_xml
149+
# # => <?xml version="1.0"?>
150+
# # <root xmlns:foo="http://nokogiri.org/default_ns/test/foo">
151+
# # <foo:parent>
152+
# # <foo:child/>
153+
# # </foo:parent>
154+
# # </root>
155+
#
156+
# @return [Boolean]
157+
#
158+
# @since v1.12.4
159+
attr_accessor :namespace_inheritance
160+
116161
def initialize *args # :nodoc:
117162
@errors = []
118163
@decorators = nil
164+
@namespace_inheritance = false
119165
end
120166

121167
##

test/xml/test_builder.rb

Lines changed: 36 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -107,9 +107,37 @@ def test_non_root_namespace
107107
assert_equal "one", b.doc.at("hello", "xmlns" => "one").namespace.href
108108
end
109109

110-
def test_builder_namespace_children_do_not_inherit
111-
# see https://github.com/sparklemotion/nokogiri/issues/1712
110+
def test_builder_namespace_inheritance_true
111+
# see https://github.com/sparklemotion/nokogiri/issues/2317
112112
result = Nokogiri::XML::Builder.new(encoding: 'utf-8') do |xml|
113+
xml["soapenv"].Envelope("xmlns:soapenv" => "http://schemas.xmlsoap.org/soap/envelope/") do
114+
xml.Header
115+
end
116+
end
117+
assert(result.doc.namespace_inheritance)
118+
assert(
119+
result.doc.at_xpath("//soapenv:Header", "soapenv" => "http://schemas.xmlsoap.org/soap/envelope/"),
120+
"header element should have a namespace"
121+
)
122+
end
123+
124+
def test_builder_namespace_inheritance_false
125+
# see https://github.com/sparklemotion/nokogiri/issues/2317
126+
result = Nokogiri::XML::Builder.new(encoding: 'utf-8', namespace_inheritance: false) do |xml|
127+
xml["soapenv"].Envelope("xmlns:soapenv" => "http://schemas.xmlsoap.org/soap/envelope/") do
128+
xml.Header
129+
end
130+
end
131+
refute(result.doc.namespace_inheritance)
132+
assert(
133+
result.doc.at_xpath("//Header"),
134+
"header element should not have a namespace"
135+
)
136+
end
137+
138+
def test_builder_namespace_inheritance_false_part_deux
139+
# see https://github.com/sparklemotion/nokogiri/issues/1712
140+
result = Nokogiri::XML::Builder.new(encoding: 'utf-8', namespace_inheritance: false) do |xml|
113141
xml['soapenv'].Envelope('xmlns:soapenv' => 'http://schemas.xmlsoap.org/soap/envelope/', 'xmlns:emer' => 'http://dashcs.com/api/v1/emergency') do
114142
xml['soapenv'].Header
115143
xml['soapenv'].Body do
@@ -197,12 +225,18 @@ def test_specified_namespace_undeclared
197225
}
198226
end
199227

228+
def test_set_namespace_inheritance
229+
assert(Nokogiri::XML::Builder.new.doc.namespace_inheritance)
230+
refute(Nokogiri::XML::Builder.new(namespace_inheritance: false).doc.namespace_inheritance)
231+
end
232+
200233
def test_set_encoding
201234
builder = Nokogiri::XML::Builder.new(:encoding => "UTF-8") do |xml|
202235
xml.root do
203236
xml.bar "blah"
204237
end
205238
end
239+
assert_equal "UTF-8", builder.doc.encoding
206240
assert_match "UTF-8", builder.to_xml
207241
end
208242

test/xml/test_document.rb

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,13 @@ class TestDocument < Nokogiri::TestCase
1515

1616
let(:xml) { Nokogiri::XML.parse(File.read(XML_FILE), XML_FILE) }
1717

18+
def test_default_namespace_inheritance
19+
doc = Nokogiri::XML::Document.new
20+
refute(doc.namespace_inheritance)
21+
doc.namespace_inheritance = true
22+
assert(doc.namespace_inheritance)
23+
end
24+
1825
def test_dtd_with_empty_internal_subset
1926
doc = Nokogiri::XML(<<~eoxml)
2027
<?xml version="1.0"?>

test/xml/test_node_reparenting.rb

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -557,6 +557,35 @@ def coerce(data)
557557
end
558558
end
559559
end
560+
561+
describe "given a parent node with a non-default namespace" do
562+
let(:doc) do
563+
Nokogiri::XML(<<~EOF)
564+
<root xmlns:foo="http://nokogiri.org/default_ns/test/foo">
565+
<foo:parent>
566+
</foo:parent>
567+
</root>
568+
EOF
569+
end
570+
let(:parent) { doc.at_xpath("//foo:parent", "foo" => "http://nokogiri.org/default_ns/test/foo") }
571+
572+
describe "and namespace_inheritance is off" do
573+
it "inserts a child node that does not inherit the parent's namespace" do
574+
refute(doc.namespace_inheritance)
575+
child = parent.add_child("<child></child>").first
576+
assert_nil(child.namespace)
577+
end
578+
end
579+
580+
describe "and namespace_inheritance is on" do
581+
it "inserts a child node that inherits the parent's namespace" do
582+
doc.namespace_inheritance = true
583+
child = parent.add_child("<child></child>").first
584+
assert_not_nil(child.namespace)
585+
assert_equal("http://nokogiri.org/default_ns/test/foo", child.namespace.href)
586+
end
587+
end
588+
end
560589
end
561590

562591
describe "#add_previous_sibling" do

0 commit comments

Comments
 (0)