1<?php
2/**
3 * Custom XML parser for signed and/or encrypted XML Docs
4 *
5 * @author  Donal McMullan  donal@catalyst.net.nz
6 * @version 0.0.1
7 * @license http://www.gnu.org/copyleft/gpl.html GNU Public License
8 * @package mnet
9 */
10
11/**
12 * Custom XML parser class for signed and/or encrypted XML Docs
13 */
14class mnet_encxml_parser {
15    /**
16     * Constructor creates and initialises parser resource and calls initialise
17     *
18     * @return bool True
19     */
20    public function __construct() {
21        return $this->initialise();
22    }
23
24    /**
25     * Old syntax of class constructor. Deprecated in PHP7.
26     *
27     * @deprecated since Moodle 3.1
28     */
29    public function mnet_encxml_parser() {
30        debugging('Use of class name as constructor is deprecated', DEBUG_DEVELOPER);
31        self::__construct();
32    }
33
34    /**
35     * Set default element handlers and initialise properties to empty.
36     *
37     * @return bool True
38     */
39    function initialise() {
40        $this->parser = xml_parser_create();
41        xml_set_object($this->parser, $this);
42
43        xml_set_element_handler($this->parser, "start_element", "end_element");
44        xml_set_character_data_handler($this->parser, "discard_data");
45
46        $this->tag_number        = 0; // Just a unique ID for each tag
47        $this->digest            = '';
48        $this->remote_timestamp  = '';
49        $this->remote_wwwroot    = '';
50        $this->signature         = '';
51        $this->data_object       = '';
52        $this->key_URI           = '';
53        $this->payload_encrypted = false;
54        $this->cipher            = array();
55        $this->error             = array();
56        $this->remoteerror       = null;
57        $this->errorstarted      = false;
58        return true;
59    }
60
61    /**
62     * Parse a block of XML text
63     *
64     * The XML Text will be an XML-RPC request which is wrapped in an XML doc
65     * with a signature from the sender. This envelope may be encrypted and
66     * delivered within another XML envelope with a symmetric key. The parser
67     * should first decrypt this XML, and then place the XML-RPC request into
68     * the data_object property, and the signature into the signature property.
69     *
70     * See the W3C's {@link http://www.w3.org/TR/xmlenc-core/ XML Encryption Syntax and Processing}
71     * and {@link http://www.w3.org/TR/2001/PR-xmldsig-core-20010820/ XML-Signature Syntax and Processing}
72     * guidelines for more detail on the XML.
73     *
74     * -----XML-Envelope---------------------------------
75     * |                                                |
76     * |    Symmetric-key--------------------------     |
77     * |    |_____________________________________|     |
78     * |                                                |
79     * |    Encrypted data-------------------------     |
80     * |    |                                     |     |
81     * |    |  -XML-Envelope------------------    |     |
82     * |    |  |                             |    |     |
83     * |    |  |  --Signature-------------   |    |     |
84     * |    |  |  |______________________|   |    |     |
85     * |    |  |                             |    |     |
86     * |    |  |  --Signed-Payload--------   |    |     |
87     * |    |  |  |                      |   |    |     |
88     * |    |  |  |   XML-RPC Request    |   |    |     |
89     * |    |  |  |______________________|   |    |     |
90     * |    |  |                             |    |     |
91     * |    |  |_____________________________|    |     |
92     * |    |_____________________________________|     |
93     * |                                                |
94     * |________________________________________________|
95     *
96     * @param   string  $data   The XML that you want to parse
97     * @return  bool            True on success - false on failure
98     */
99    function parse($data) {
100        $p = xml_parse($this->parser, $data);
101
102        if ($p == 0) {
103            // Parse failed
104            $errcode = xml_get_error_code($this->parser);
105            $errstring = xml_error_string($errcode);
106            $lineno = xml_get_current_line_number($this->parser);
107            if ($lineno !== false) {
108                $error = array('lineno' => $lineno);
109                $lineno--; // Line numbering starts at 1.
110                while ($lineno > 0) {
111                    $data = strstr($data, "\n");
112                    $lineno--;
113                }
114                $data .= "\n"; // In case there's only one line (no newline)
115                $line = substr($data, 0, strpos($data, "\n"));
116                $error['code']   = $errcode;
117                $error['string'] = $errstring;
118                $error['line']   = $line;
119                $this->error[] = $error;
120            } else {
121                $this->error[] = array('code' => $errcode, 'string' => $errstring);
122            }
123        }
124
125        if (!empty($this->remoteerror)) {
126            return false;
127        }
128
129        if (count($this->cipher) > 0) {
130            $this->cipher = array_values($this->cipher);
131            $this->payload_encrypted = true;
132        }
133
134        return (bool)$p;
135    }
136
137    /**
138     * Destroy the parser and free up any related resource.
139     */
140    function free_resource() {
141        $free = xml_parser_free($this->parser);
142    }
143
144    /**
145     * Set the character-data handler to the right function for each element
146     *
147     * For each tag (element) name, this function switches the character-data
148     * handler to the function that handles that element. Note that character
149     * data is referred to the handler in blocks of 1024 bytes.
150     *
151     * @param   mixed   $parser The XML parser
152     * @param   string  $name   The name of the tag, e.g. method_call
153     * @param   array   $attrs  The tag's attributes (if any exist).
154     * @return  bool            True
155     */
156    function start_element($parser, $name, $attrs) {
157        $this->tag_number++;
158        $handler = 'discard_data';
159        switch(strtoupper($name)) {
160            case 'DIGESTVALUE':
161                $handler = 'parse_digest';
162                break;
163            case 'SIGNATUREVALUE':
164                $handler = 'parse_signature';
165                break;
166            case 'OBJECT':
167                $handler = 'parse_object';
168                break;
169            case 'RETRIEVALMETHOD':
170                $this->key_URI = $attrs['URI'];
171                break;
172            case 'TIMESTAMP':
173                $handler = 'parse_timestamp';
174                break;
175            case 'WWWROOT':
176                $handler = 'parse_wwwroot';
177                break;
178            case 'CIPHERVALUE':
179                $this->cipher[$this->tag_number] = '';
180                $handler = 'parse_cipher';
181                break;
182            case 'FAULT':
183                $handler = 'parse_fault';
184            default:
185                break;
186        }
187        xml_set_character_data_handler($this->parser, $handler);
188        return true;
189    }
190
191    /**
192     * Add the next chunk of character data to the remote_timestamp string
193     *
194     * @param   mixed   $parser The XML parser
195     * @param   string  $data   The content of the current tag (1024 byte chunk)
196     * @return  bool            True
197     */
198    function parse_timestamp($parser, $data) {
199        $this->remote_timestamp .= $data;
200        return true;
201    }
202
203    /**
204     * Add the next chunk of character data to the cipher string for that tag
205     *
206     * The XML parser calls the character-data handler with 1024-character
207     * chunks of data. This means that the handler may be called several times
208     * for a single tag, so we use the concatenate operator (.) to build the
209     * tag content into a string.
210     * We should not encounter more than one of each tag type, except for the
211     * cipher tag. We will often see two of those. We prevent the content of
212     * these two tags being concatenated together by counting each tag, and
213     * using its 'number' as the key to an array of ciphers.
214     *
215     * @param   mixed   $parser The XML parser
216     * @param   string  $data   The content of the current tag (1024 byte chunk)
217     * @return  bool            True
218     */
219    function parse_cipher($parser, $data) {
220        $this->cipher[$this->tag_number] .= $data;
221        return true;
222    }
223
224    /**
225     * Add the next chunk of character data to the remote_wwwroot string
226     *
227     * @param   mixed   $parser The XML parser
228     * @param   string  $data   The content of the current tag (1024 byte chunk)
229     * @return  bool            True
230     */
231    function parse_wwwroot($parser, $data) {
232        $this->remote_wwwroot .= $data;
233        return true;
234    }
235
236    /**
237     * Add the next chunk of character data to the digest string
238     *
239     * @param   mixed   $parser The XML parser
240     * @param   string  $data   The content of the current tag (1024 byte chunk)
241     * @return  bool            True
242     */
243    function parse_digest($parser, $data) {
244        $this->digest .= $data;
245        return true;
246    }
247
248    /**
249     * Add the next chunk of character data to the signature string
250     *
251     * @param   mixed   $parser The XML parser
252     * @param   string  $data   The content of the current tag (1024 byte chunk)
253     * @return  bool            True
254     */
255    function parse_signature($parser, $data) {
256        $this->signature .= $data;
257        return true;
258    }
259
260    /**
261     * Add the next chunk of character data to the data_object string
262     *
263     * @param   mixed   $parser The XML parser
264     * @param   string  $data   The content of the current tag (1024 byte chunk)
265     * @return  bool            True
266     */
267    function parse_object($parser, $data) {
268        $this->data_object .= $data;
269        return true;
270    }
271
272    /**
273     * Discard the next chunk of character data
274     *
275     * This is used for tags that we're not interested in.
276     *
277     * @param   mixed   $parser The XML parser
278     * @param   string  $data   The content of the current tag (1024 byte chunk)
279     * @return  bool            True
280     */
281    function discard_data($parser, $data) {
282        if (!$this->errorstarted) {
283            // Not interested
284            return true;
285        }
286        $data = trim($data);
287        if (isset($this->errorstarted->faultstringstarted) && !empty($data)) {
288            $this->remoteerror .= ', message: ' . $data;
289        } else if (isset($this->errorstarted->faultcodestarted)) {
290            $this->remoteerror = 'code: ' . $data;
291            unset($this->errorstarted->faultcodestarted);
292        } else if ($data == 'faultCode') {
293            $this->errorstarted->faultcodestarted = true;
294        } else if ($data == 'faultString') {
295            $this->errorstarted->faultstringstarted = true;
296        }
297        return true;
298
299    }
300
301    function parse_fault($parser, $data) {
302        $this->errorstarted = new StdClass;
303        return true;
304    }
305
306    /**
307     * Switch the character-data handler to ignore the next chunk of data
308     *
309     * @param   mixed   $parser The XML parser
310     * @param   string  $name   The name of the tag, e.g. method_call
311     * @return  bool            True
312     */
313    function end_element($parser, $name) {
314        $ok = xml_set_character_data_handler($this->parser, "discard_data");
315        return true;
316    }
317}
318