summaryrefslogtreecommitdiff
path: root/lib/rexml/entity.rb
blob: d9a72cc8fa147bf18f47008b64f420bb7b903150 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
# frozen_string_literal: false
require 'rexml/child'
require 'rexml/source'
require 'rexml/xmltokens'

module REXML
  # God, I hate DTDs.  I really do.  Why this idiot standard still
  # plagues us is beyond me.
  class Entity < Child
    include XMLTokens
    PUBIDCHAR = "\x20\x0D\x0Aa-zA-Z0-9\\-()+,./:=?;!*@$_%#"
    SYSTEMLITERAL = %Q{((?:"[^"]*")|(?:'[^']*'))}
    PUBIDLITERAL = %Q{("[#{PUBIDCHAR}']*"|'[#{PUBIDCHAR}]*')}
    EXTERNALID = "(?:(?:(SYSTEM)\\s+#{SYSTEMLITERAL})|(?:(PUBLIC)\\s+#{PUBIDLITERAL}\\s+#{SYSTEMLITERAL}))"
    NDATADECL = "\\s+NDATA\\s+#{NAME}"
    PEREFERENCE = "%#{NAME};"
    ENTITYVALUE = %Q{((?:"(?:[^%&"]|#{PEREFERENCE}|#{REFERENCE})*")|(?:'([^%&']|#{PEREFERENCE}|#{REFERENCE})*'))}
    PEDEF = "(?:#{ENTITYVALUE}|#{EXTERNALID})"
    ENTITYDEF = "(?:#{ENTITYVALUE}|(?:#{EXTERNALID}(#{NDATADECL})?))"
    PEDECL = "<!ENTITY\\s+(%)\\s+#{NAME}\\s+#{PEDEF}\\s*>"
    GEDECL = "<!ENTITY\\s+#{NAME}\\s+#{ENTITYDEF}\\s*>"
    ENTITYDECL = /\s*(?:#{GEDECL})|(?:#{PEDECL})/um

    attr_reader :name, :external, :ref, :ndata, :pubid

    # Create a new entity.  Simple entities can be constructed by passing a
    # name, value to the constructor; this creates a generic, plain entity
    # reference. For anything more complicated, you have to pass a Source to
    # the constructor with the entity definition, or use the accessor methods.
    # +WARNING+: There is no validation of entity state except when the entity
    # is read from a stream.  If you start poking around with the accessors,
    # you can easily create a non-conformant Entity.  The best thing to do is
    # dump the stupid DTDs and use XMLSchema instead.
    #
    #  e = Entity.new( 'amp', '&' )
    def initialize stream, value=nil, parent=nil, reference=false
      super(parent)
      @ndata = @pubid = @value = @external = nil
      if stream.kind_of? Array
        @name = stream[1]
        if stream[-1] == '%'
          @reference = true
          stream.pop
        else
          @reference = false
        end
        if stream[2] =~ /SYSTEM|PUBLIC/
          @external = stream[2]
          if @external == 'SYSTEM'
            @ref = stream[3]
            @ndata = stream[4] if stream.size == 5
          else
            @pubid = stream[3]
            @ref = stream[4]
          end
        else
          @value = stream[2]
        end
      else
        @reference = reference
        @external = nil
        @name = stream
        @value = value
      end
    end

    # Evaluates whether the given string matches an entity definition,
    # returning true if so, and false otherwise.
    def Entity::matches? string
      (ENTITYDECL =~ string) == 0
    end

    # Evaluates to the unnormalized value of this entity; that is, replacing
    # all entities -- both %ent; and &ent; entities.  This differs from
    # +value()+ in that +value+ only replaces %ent; entities.
    def unnormalized
      document.record_entity_expansion unless document.nil?
      v = value()
      return nil if v.nil?
      @unnormalized = Text::unnormalize(v, parent)
      @unnormalized
    end

    #once :unnormalized

    # Returns the value of this entity unprocessed -- raw.  This is the
    # normalized value; that is, with all %ent; and &ent; entities intact
    def normalized
      @value
    end

    # Write out a fully formed, correct entity definition (assuming the Entity
    # object itself is valid.)
    #
    # out::
    #   An object implementing <TT>&lt;&lt;<TT> to which the entity will be
    #   output
    # indent::
    #   *DEPRECATED* and ignored
    def write out, indent=-1
      out << '<!ENTITY '
      out << '% ' if @reference
      out << @name
      out << ' '
      if @external
        out << @external << ' '
        if @pubid
          q = @pubid.include?('"')?"'":'"'
          out << q << @pubid << q << ' '
        end
        q = @ref.include?('"')?"'":'"'
        out << q << @ref << q
        out << ' NDATA ' << @ndata if @ndata
      else
        q = @value.include?('"')?"'":'"'
        out << q << @value << q
      end
      out << '>'
    end

    # Returns this entity as a string.  See write().
    def to_s
      rv = ''
      write rv
      rv
    end

    PEREFERENCE_RE = /#{PEREFERENCE}/um
    # Returns the value of this entity.  At the moment, only internal entities
    # are processed.  If the value contains internal references (IE,
    # %blah;), those are replaced with their values.  IE, if the doctype
    # contains:
    #  <!ENTITY % foo "bar">
    #  <!ENTITY yada "nanoo %foo; nanoo>
    # then:
    #  doctype.entity('yada').value   #-> "nanoo bar nanoo"
    def value
      if @value
        matches = @value.scan(PEREFERENCE_RE)
        rv = @value.clone
        if @parent
          sum = 0
          matches.each do |entity_reference|
            entity_value = @parent.entity( entity_reference[0] )
            if sum + entity_value.bytesize > Security.entity_expansion_text_limit
              raise "entity expansion has grown too large"
            else
              sum += entity_value.bytesize
            end
            rv.gsub!( /%#{entity_reference.join};/um, entity_value )
          end
        end
        return rv
      end
      nil
    end
  end

  # This is a set of entity constants -- the ones defined in the XML
  # specification.  These are +gt+, +lt+, +amp+, +quot+ and +apos+.
  # CAUTION: these entities does not have parent and document
  module EntityConst
    # +>+
    GT = Entity.new( 'gt', '>' )
    # +<+
    LT = Entity.new( 'lt', '<' )
    # +&+
    AMP = Entity.new( 'amp', '&' )
    # +"+
    QUOT = Entity.new( 'quot', '"' )
    # +'+
    APOS = Entity.new( 'apos', "'" )
  end
end