1<?php
2// This file is part of Moodle - http://moodle.org/
3//
4// Moodle is free software: you can redistribute it and/or modify
5// it under the terms of the GNU General Public License as published by
6// the Free Software Foundation, either version 3 of the License, or
7// (at your option) any later version.
8//
9// Moodle is distributed in the hope that it will be useful,
10// but WITHOUT ANY WARRANTY; without even the implied warranty of
11// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12// GNU General Public License for more details.
13//
14// You should have received a copy of the GNU General Public License
15// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
16
17/**
18 * @package   core_backup
19 * @category  phpunit
20 * @copyright 2010 onwards Eloy Lafuente (stronk7) {@link http://stronk7.com}
21 * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
22 */
23
24defined('MOODLE_INTERNAL') || die();
25
26// Include all the needed stuff
27global $CFG;
28require_once($CFG->dirroot . '/backup/util/xml/xml_writer.class.php');
29require_once($CFG->dirroot . '/backup/util/xml/output/xml_output.class.php');
30require_once($CFG->dirroot . '/backup/util/xml/output/memory_xml_output.class.php');
31require_once($CFG->dirroot . '/backup/util/xml/contenttransformer/xml_contenttransformer.class.php');
32
33/**
34 * xml_writer tests
35 */
36class xml_writer_testcase extends basic_testcase {
37
38    /**
39     * test xml_writer public methods
40     */
41    function test_xml_writer_public_api() {
42        global $CFG;
43        // Instantiate xml_output
44        $xo = new memory_xml_output();
45        $this->assertTrue($xo instanceof xml_output);
46
47        // Instantiate xml_writer with null xml_output
48        try {
49            $xw = new mock_xml_writer(null);
50            $this->assertTrue(false, 'xml_writer_exception expected');
51        } catch (exception $e) {
52            $this->assertTrue($e instanceof xml_writer_exception);
53            $this->assertEquals($e->errorcode, 'invalid_xml_output');
54        }
55
56        // Instantiate xml_writer with wrong xml_output object
57        try {
58            $xw = new mock_xml_writer(new stdclass());
59            $this->assertTrue(false, 'xml_writer_exception expected');
60        } catch (exception $e) {
61            $this->assertTrue($e instanceof xml_writer_exception);
62            $this->assertEquals($e->errorcode, 'invalid_xml_output');
63        }
64
65        // Instantiate xml_writer with wrong xml_contenttransformer object
66        try {
67            $xw = new mock_xml_writer($xo, new stdclass());
68            $this->assertTrue(false, 'xml_writer_exception expected');
69        } catch (exception $e) {
70            $this->assertTrue($e instanceof xml_writer_exception);
71            $this->assertEquals($e->errorcode, 'invalid_xml_contenttransformer');
72        }
73
74        // Instantiate xml_writer and start it twice
75        $xw = new mock_xml_writer($xo);
76        $xw->start();
77        try {
78            $xw->start();
79            $this->assertTrue(false, 'xml_writer_exception expected');
80        } catch (exception $e) {
81            $this->assertTrue($e instanceof xml_writer_exception);
82            $this->assertEquals($e->errorcode, 'xml_writer_already_started');
83        }
84
85        // Instantiate xml_writer and stop it twice
86        $xo = new memory_xml_output();
87        $xw = new mock_xml_writer($xo);
88        $xw->start();
89        $xw->stop();
90        try {
91            $xw->stop();
92            $this->assertTrue(false, 'xml_writer_exception expected');
93        } catch (exception $e) {
94            $this->assertTrue($e instanceof xml_writer_exception);
95            $this->assertEquals($e->errorcode, 'xml_writer_already_stopped');
96        }
97
98        // Stop writer without starting it
99        $xo = new memory_xml_output();
100        $xw = new mock_xml_writer($xo);
101        try {
102            $xw->stop();
103            $this->assertTrue(false, 'xml_writer_exception expected');
104        } catch (exception $e) {
105            $this->assertTrue($e instanceof xml_writer_exception);
106            $this->assertEquals($e->errorcode, 'xml_writer_not_started');
107        }
108
109        // Start writer after stopping it
110        $xo = new memory_xml_output();
111        $xw = new mock_xml_writer($xo);
112        $xw->start();
113        $xw->stop();
114        try {
115            $xw->start();
116            $this->assertTrue(false, 'xml_writer_exception expected');
117        } catch (exception $e) {
118            $this->assertTrue($e instanceof xml_writer_exception);
119            $this->assertEquals($e->errorcode, 'xml_writer_already_stopped');
120        }
121
122        // Try to set prologue/schema after start
123        $xo = new memory_xml_output();
124        $xw = new mock_xml_writer($xo);
125        $xw->start();
126        try {
127            $xw->set_nonamespace_schema('http://moodle.org');
128            $this->assertTrue(false, 'xml_writer_exception expected');
129        } catch (exception $e) {
130            $this->assertTrue($e instanceof xml_writer_exception);
131            $this->assertEquals($e->errorcode, 'xml_writer_already_started');
132        }
133        try {
134            $xw->set_prologue('sweet prologue');
135            $this->assertTrue(false, 'xml_writer_exception expected');
136        } catch (exception $e) {
137            $this->assertTrue($e instanceof xml_writer_exception);
138            $this->assertEquals($e->errorcode, 'xml_writer_already_started');
139        }
140
141        // Instantiate properly with memory_xml_output, start and stop.
142        // Must get default UTF-8 prologue
143        $xo = new memory_xml_output();
144        $xw = new mock_xml_writer($xo);
145        $xw->start();
146        $xw->stop();
147        $this->assertEquals($xo->get_allcontents(), $xw->get_default_prologue());
148
149        // Instantiate, set prologue and schema, put 1 full tag and get results
150        $xo = new memory_xml_output();
151        $xw = new mock_xml_writer($xo);
152        $xw->set_prologue('CLEARLY WRONG PROLOGUE');
153        $xw->set_nonamespace_schema('http://moodle.org/littleschema');
154        $xw->start();
155        $xw->full_tag('TEST', 'Hello World!', array('id' => 1));
156        $xw->stop();
157        $result = $xo->get_allcontents();
158        // Perform various checks
159        $this->assertEquals(strpos($result, 'WRONG'), 8);
160        $this->assertEquals(strpos($result, '<TEST id="1"'), 22);
161        $this->assertEquals(strpos($result, 'xmlns:xsi='), 39);
162        $this->assertEquals(strpos($result, 'http://moodle.org/littleschema'), 128);
163        $this->assertEquals(strpos($result, 'Hello World'), 160);
164        $this->assertFalse(strpos($result, $xw->get_default_prologue()));
165
166        // Try to close one tag in wrong order
167        $xo = new memory_xml_output();
168        $xw = new mock_xml_writer($xo);
169        $xw->start();
170        $xw->begin_tag('first');
171        $xw->begin_tag('second');
172        try {
173            $xw->end_tag('first');
174            $this->assertTrue(false, 'xml_writer_exception expected');
175        } catch (exception $e) {
176            $this->assertTrue($e instanceof xml_writer_exception);
177            $this->assertEquals($e->errorcode, 'xml_writer_end_tag_no_match');
178        }
179
180        // Try to close one tag before starting any tag
181        $xo = new memory_xml_output();
182        $xw = new mock_xml_writer($xo);
183        $xw->start();
184        try {
185            $xw->end_tag('first');
186            $this->assertTrue(false, 'xml_writer_exception expected');
187        } catch (exception $e) {
188            $this->assertTrue($e instanceof xml_writer_exception);
189            $this->assertEquals($e->errorcode, 'xml_writer_end_tag_no_match');
190        }
191
192        // Full tag without contents (null and empty string)
193        $xo = new memory_xml_output();
194        $xw = new mock_xml_writer($xo);
195        $xw->set_prologue(''); // empty prologue for easier matching
196        $xw->start();
197        $xw->full_tag('tagname', null, array('attrname' => 'attrvalue'));
198        $xw->full_tag('tagname2', '', array('attrname' => 'attrvalue'));
199        $xw->stop();
200        $result = $xo->get_allcontents();
201        $this->assertEquals($result, '<tagname attrname="attrvalue" /><tagname2 attrname="attrvalue"></tagname2>');
202
203
204        // Test case-folding is working
205        $xo = new memory_xml_output();
206        $xw = new mock_xml_writer($xo, null, true);
207        $xw->set_prologue(''); // empty prologue for easier matching
208        $xw->start();
209        $xw->full_tag('tagname', 'textcontent', array('attrname' => 'attrvalue'));
210        $xw->stop();
211        $result = $xo->get_allcontents();
212        $this->assertEquals($result, '<TAGNAME ATTRNAME="attrvalue">textcontent</TAGNAME>');
213
214        // Test UTF-8 chars in tag and attribute names, attr values and contents
215        $xo = new memory_xml_output();
216        $xw = new mock_xml_writer($xo);
217        $xw->set_prologue(''); // empty prologue for easier matching
218        $xw->start();
219        $xw->full_tag('áéíóú', 'ÁÉÍÓÚ', array('àèìòù' => 'ÀÈÌÒÙ'));
220        $xw->stop();
221        $result = $xo->get_allcontents();
222        $this->assertEquals($result, '<áéíóú àèìòù="ÀÈÌÒÙ">ÁÉÍÓÚ</áéíóú>');
223
224        // Try non-safe content in attributes
225        $xo = new memory_xml_output();
226        $xw = new mock_xml_writer($xo);
227        $xw->set_prologue(''); // empty prologue for easier matching
228        $xw->start();
229        $xw->full_tag('tagname', 'textcontent', array('attrname' => 'attr' . chr(27) . '\'"value'));
230        $xw->stop();
231        $result = $xo->get_allcontents();
232        $this->assertEquals($result, '<tagname attrname="attr\'&quot;value">textcontent</tagname>');
233
234        // Try non-safe content in text
235        $xo = new memory_xml_output();
236        $xw = new mock_xml_writer($xo);
237        $xw->set_prologue(''); // empty prologue for easier matching
238        $xw->start();
239        $xw->full_tag('tagname', "text\r\ncontent\rwith" . chr(27), array('attrname' => 'attrvalue'));
240        $xw->stop();
241        $result = $xo->get_allcontents();
242        $this->assertEquals($result, '<tagname attrname="attrvalue">text' . "\ncontent\n" . 'with</tagname>');
243
244        // Try to stop the writer without clossing all the open tags
245        $xo = new memory_xml_output();
246        $xw = new mock_xml_writer($xo);
247        $xw->start();
248        $xw->begin_tag('first');
249        try {
250            $xw->stop();
251            $this->assertTrue(false, 'xml_writer_exception expected');
252        } catch (exception $e) {
253            $this->assertTrue($e instanceof xml_writer_exception);
254            $this->assertEquals($e->errorcode, 'xml_writer_open_tags_remaining');
255        }
256
257        // Test simple transformer
258        $xo = new memory_xml_output();
259        $xt = new mock_xml_contenttransformer();
260        $xw = new mock_xml_writer($xo, $xt);
261        $xw->set_prologue(''); // empty prologue for easier matching
262        $xw->start();
263        $xw->full_tag('tagname', null, array('attrname' => 'attrvalue'));
264        $xw->full_tag('tagname2', 'somecontent', array('attrname' => 'attrvalue'));
265        $xw->stop();
266        $result = $xo->get_allcontents();
267        $this->assertEquals($result, '<tagname attrname="attrvalue" /><tagname2 attrname="attrvalue">testsomecontent</tagname2>');
268
269        // Build a complex XML file and test results against stored file in fixtures
270        $xo = new memory_xml_output();
271        $xw = new mock_xml_writer($xo);
272        $xw->start();
273        $xw->begin_tag('toptag', array('name' => 'toptag', 'level' => 1, 'path' => '/toptag'));
274        $xw->full_tag('secondtag', 'secondvalue', array('name' => 'secondtag', 'level' => 2, 'path' => '/toptag/secondtag', 'value' => 'secondvalue'));
275        $xw->begin_tag('thirdtag', array('name' => 'thirdtag', 'level' => 2, 'path' => '/toptag/thirdtag'));
276        $xw->full_tag('onevalue', 'onevalue', array('name' => 'onevalue', 'level' => 3, 'path' => '/toptag/thirdtag/onevalue'));
277        $xw->full_tag('onevalue', 'anothervalue', array('name' => 'onevalue', 'level' => 3, 'value' => 'anothervalue'));
278        $xw->full_tag('onevalue', 'yetanothervalue', array('name' => 'onevalue', 'level' => 3, 'value' => 'yetanothervalue'));
279        $xw->full_tag('twovalue', 'twovalue', array('name' => 'twovalue', 'level' => 3, 'path' => '/toptag/thirdtag/twovalue'));
280        $xw->begin_tag('forthtag', array('name' => 'forthtag', 'level' => 3, 'path' => '/toptag/thirdtag/forthtag'));
281        $xw->full_tag('innervalue', 'innervalue');
282        $xw->begin_tag('innertag');
283        $xw->begin_tag('superinnertag', array('name' => 'superinnertag', 'level' => 5));
284        $xw->full_tag('superinnervalue', 'superinnervalue', array('name' => 'superinnervalue', 'level' => 6));
285        $xw->end_tag('superinnertag');
286        $xw->end_tag('innertag');
287        $xw->end_tag('forthtag');
288        $xw->begin_tag('fifthtag', array('level' => 3));
289        $xw->begin_tag('sixthtag', array('level' => 4));
290        $xw->full_tag('seventh', 'seventh', array('level' => 5));
291        $xw->end_tag('sixthtag');
292        $xw->end_tag('fifthtag');
293        $xw->full_tag('finalvalue', 'finalvalue', array('name' => 'finalvalue', 'level' => 3, 'path' => '/toptag/thirdtag/finalvalue'));
294        $xw->full_tag('finalvalue');
295        $xw->end_tag('thirdtag');
296        $xw->end_tag('toptag');
297        $xw->stop();
298        $result = $xo->get_allcontents();
299        $fcontents = file_get_contents($CFG->dirroot . '/backup/util/xml/tests/fixtures/test1.xml');
300
301        // Normalise carriage return characters.
302        $fcontents = str_replace("\r\n", "\n", $fcontents);
303        $this->assertEquals(trim($result), trim($fcontents));
304    }
305}
306
307/*
308 * helper extended xml_writer class that makes some methods public for testing
309 */
310class mock_xml_writer extends xml_writer {
311    public function get_default_prologue() {
312        return parent::get_default_prologue();
313    }
314}
315
316/*
317 * helper extended xml_contenttransformer prepending "test" to all the notnull contents
318 */
319class mock_xml_contenttransformer extends xml_contenttransformer {
320    public function process($content) {
321        return is_null($content) ? null : 'test' . $content;
322    }
323}
324