Bug #15057
closedREXML::Text#value returns a double unescaped string in non-raw mode
Description
REXML::Text
オブジェクトが非rawモードの場合、REXML::Text#value
がエスケープ済みのテキストを二重にエスケープ解除された文字列を返します。
例:
require 'rexml/document'
t = REXML::Text.new("< <", false, nil, false)
t.to_s # => "&lt; <"
t.value # => "< <" (expected: "< <")
REXML::Text#value
のコメントに以下のような記述があるため、上の挙動は期待通りのように見えますが、このコメントそのものが誤りだと思います。
# t = Text.new( "< & &s; russell", false, nil, false )
# t.value #-> "< & sean russell"
非rawモードではコンストラクタの第一引数に渡された文字列はテキストノードが表す文字列そのものを意味するはずです。上で渡された文字列中の "&s;" は実体参照ではなく単なる3文字のテキストを意味します。t.value
は "< & &s; russell" であるべきだと思います。
Updated by kou (Kouhei Sutou) over 6 years ago
- Status changed from Open to Feedback
- Assignee set to kou (Kouhei Sutou)
DOMでもnodeValue
の値は実体参照を解決した値になるので今の挙動で問題ないように思うんですが、どんなユースケースで使おうとして今の挙動は間違っていると考えたか教えてもらえますか?
非rawモードではコンストラクタの第一引数に渡された文字列はテキストノードが表す文字列そのものを意味するはずです。
こうするべきだと考えた理由というか。
Updated by rna (Ryosuke Nanba) over 6 years ago
DOMでもnodeValueの値は実体参照を解決した値になるので今の挙動で問題ないように思うんですが、
JavaScript の DOM でも Text
のコンストラクタ引数で渡した文字列はパースの対象にならずにそのままテキストノードが表す文字列になります。
<p id="p"></p>
<script>
t = new Text("< <");
console.log(t.nodeValue); // コンソールに「< <」が出力
p.appendChild(t); // 画面に「< <」と表示。
</script>
実体参照が解決されるのは XML 文書実体(ソースコード)からパースする段階の話で、DOM API としては通常パース済みの文字列しか扱いません。
DOM はともかくとして、REXMLの仕様として(非rawモードで) Text
のコンストラクタ引数はパースされることになっているのだ、ということであれば、以下の挙動はバグということになってしまいます。
t = REXML::Text.new("< <", false, nil, false)
t.to_s # => "&lt; <" (expected?: "< <" (or Error?))
どんなユースケースで使おうとして今の挙動は間違っていると考えたか教えてもらえますか?
atomutil 0.1.4 でテキストノードを要素から要素にコピーするユースケースでエスケープが解除されてしまってコピーにならないという現象が発生していました。
https://github.com/lyokato/ruby-atomutil/blob/954c5d1b387bff63db3462d19d3f7abf196d2b1f/lib/atomutil.rb#L526 (element
と value.elem
が REXML::Element
です):
element.text = value.elem.text unless value.elem.text.nil?
Updated by kou (Kouhei Sutou) over 6 years ago
なるほど。
atomutilでそうなるケースを試してみたいので再現するAtomとサンプルコードを提供してもらえませんか?
Updated by rna (Ryosuke Nanba) over 6 years ago
atomutilでそうなるケースを試してみたいので再現するAtomとサンプルコードを提供してもらえませんか?
これでどうでしょう?
require 'atomutil'
entry = Atom::Entry.new
entry.content = "<br>"
puts entry.to_s
entry.to_s は実際に投稿する場合 HTTP の request body になります(参照: Atom::Client#create_resource)。
上のサンプルは「HTMLタグを記述可能なテキスト形式での投稿を受理するブログサービス(はてなブログのはてな記法モード等)で、HTMLタグ
の説明を書くためタグをエスケープして記述した」という想定です。ブログサービス側では atom の content 要素の内容をパースしたものをテキスト形式の投稿データとして扱います。
期待する結果はブログエントリに br タグのソースコードが表示されることですが、実際には改行が表示されます(本物の br タグとして機能してしまう)。
ちなみに本件は元々 HatenaBlogWriter v0.5 https://github.com/rnanba/HatenaBlogWriter/tree/v0.5 に対する不具合報告が発端です(master には現在 monkey patch による回避コードが入っています)。はてなブログで実際に試す場合はこちらを参考にしてください。
Updated by kou (Kouhei Sutou) over 6 years ago
ありがとうございます。確認できました。
現状の動きが変だという気持ちはわかるのですが、Text#value
のドキュメントに
This ignores the 'raw' attribute setting
と書いているので、raw
のときもそうじゃないときも期待した挙動にするのは、互換性を維持したままではムリなんですよねぇ。
なので、element1.text = element2.text
という使い方がよくないんですよねぇ。ただ、こう書けたほうがうれしいので、これに匹敵する使い勝手のAPIを考えておきます。
互換性は壊したくないので、現状ではatomutilでは次のように使ってもらいたいです。
diff --git a/lib/atomutil.rb b/lib/atomutil.rb
index 9bb9e0c..6d9b985 100644
--- a/lib/atomutil.rb
+++ b/lib/atomutil.rb
@@ -523,7 +523,10 @@ module Atom
element.add_attribute a
end
end
- element.text = value.elem.text unless value.elem.text.nil?
+ text = value.elem.get_text
+ unless text.nil?
+ element.text = REXML::Text.new(text.to_s, true, nil, true)
+ end
else
if value.is_a?(REXML::Element)
element.add_element value.deep_clone
Updated by rna (Ryosuke Nanba) over 6 years ago
This ignores the 'raw' attribute setting
と書いているので、rawのときもそうじゃないときも期待した挙動にするのは、互換性を維持したままではムリなんですよねぇ。
なので、element1.text = element2.textという使い方がよくないんですよねぇ。ただ、こう書けたほうがうれしいので、これに匹敵する使い勝手のAPIを考えておきます。
なるほど。REXML::Element#text
も同様のコメントがあってセマンティクスを変えられないと。残念ですが仕方がないですかね… メジャーバージョンアップの際は再検討していただけると幸いです。
互換性は壊したくないので、現状ではatomutilでは次のように使ってもらいたいです。
了解しました。atomutil の方にはこちらから報告しておきます。
Updated by kou (Kouhei Sutou) over 6 years ago
- Status changed from Feedback to Rejected
了解しました。atomutil の方にはこちらから報告しておきます。
ありがとうございます。では、このチケットはクローズしておきます。