MDL-35603 - Backup - Course import selector notice
[moodle.git] / backup / util / xml / xml_writer.class.php
1 <?php
3 // This file is part of Moodle - http://moodle.org/
4 //
5 // Moodle is free software: you can redistribute it and/or modify
6 // it under the terms of the GNU General Public License as published by
7 // the Free Software Foundation, either version 3 of the License, or
8 // (at your option) any later version.
9 //
10 // Moodle is distributed in the hope that it will be useful,
11 // but WITHOUT ANY WARRANTY; without even the implied warranty of
12 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13 // GNU General Public License for more details.
14 //
15 // You should have received a copy of the GNU General Public License
16 // along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
18 /**
19  * @package moodlecore
20  * @subpackage backup-xml
21  * @copyright 2010 onwards Eloy Lafuente (stronk7) {@link http://stronk7.com}
22  * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
23  */
25 /**
26  * Class implementing one (more or less complete) UTF-8 XML writer
27  *
28  * General purpose class used to output UTF-8 XML contents easily. Can be customized
29  * using implementations of @xml_output (to define where to send the xml) and
30  * and @xml_contenttransformer (to perform any transformation in contents before
31  * outputting the XML).
32  *
33  * Has support for attributes, basic w3c xml schemas declaration,
34  * and performs some content cleaning to avoid potential incorret UTF-8
35  * mess and has complete exception support.
36  *
37  * TODO: Provide UTF-8 safe strtoupper() function if using casefolding and non-ascii tags/attrs names
38  * TODO: Finish phpdocs
39  */
40 class xml_writer {
42     protected $output;     // @xml_output that defines how to output XML
43     protected $contenttransformer; // @xml_contenttransformer to modify contents before output
45     protected $prologue;   // Complete string prologue we want to use
46     protected $xmlschema;  // URI to nonamespaceschema to be added to main tag
48     protected $casefolding; // To define if xml tags must be uppercase (true) or not (false)
50     protected $level;      // current number of open tags, useful for indent text
51     protected $opentags;   // open tags accumulator, to check for errors
52     protected $lastwastext;// to know when we are writing after text content
53     protected $nullcontent;// to know if we are going to write one tag with null content
55     protected $running; // To know if writer is running
57     public function __construct($output, $contenttransformer = null, $casefolding = false) {
58         if (!$output instanceof xml_output) {
59             throw new xml_writer_exception('invalid_xml_output');
60         }
61         if (!is_null($contenttransformer) && !$contenttransformer instanceof xml_contenttransformer) {
62             throw new xml_writer_exception('invalid_xml_contenttransformer');
63         }
65         $this->output = $output;
66         $this->contenttransformer = $contenttransformer;
68         $this->prologue  = null;
69         $this->xmlschema = null;
71         $this->casefolding = $casefolding;
73         $this->level    = 0;
74         $this->opentags = array();
75         $this->lastwastext = false;
76         $this->nullcontent = false;
78         $this->running = null;
79     }
81     /**
82      * Initializes the XML writer, preparing it to accept instructions, also
83      * invoking the underlying @xml_output init method to be ready for operation
84      */
85     public function start() {
86         if ($this->running === true) {
87             throw new xml_writer_exception('xml_writer_already_started');
88         }
89         if ($this->running === false) {
90             throw new xml_writer_exception('xml_writer_already_stopped');
91         }
92         $this->output->start(); // Initialize whatever we need in output
93         if (!is_null($this->prologue)) { // Output prologue
94             $this->write($this->prologue);
95         } else {
96             $this->write($this->get_default_prologue());
97         }
98         $this->running = true;
99     }
101     /**
102      * Finishes the XML writer, not accepting instructions any more, also
103      * invoking the underlying @xml_output finish method to close/flush everything as needed
104      */
105     public function stop() {
106         if (is_null($this->running)) {
107             throw new xml_writer_exception('xml_writer_not_started');
108         }
109         if ($this->running === false) {
110             throw new xml_writer_exception('xml_writer_already_stopped');
111         }
112         if ($this->level > 0) { // Cannot stop if not at level 0, remaining open tags
113             throw new xml_writer_exception('xml_writer_open_tags_remaining');
114         }
115         $this->output->stop();
116         $this->running = false;
117     }
119     /**
120      * Set the URI location for the *nonamespace* schema to be used by the (whole) XML document
121      */
122     public function set_nonamespace_schema($uri) {
123         if ($this->running) {
124             throw new xml_writer_exception('xml_writer_already_started');
125         }
126         $this->xmlschema = $uri;
127     }
129     /**
130      * Define the complete prologue to be used, replacing the simple, default one
131      */
132     public function set_prologue($prologue) {
133         if ($this->running) {
134             throw new xml_writer_exception('xml_writer_already_started');
135         }
136         $this->prologue = $prologue;
137     }
139     /**
140      * Outputs one XML start tag with optional attributes (name => value array)
141      */
142     public function begin_tag($tag, $attributes = null) {
143         // TODO: chek the tag name is valid
144         $pre = $this->level ? "\n" . str_repeat(' ', $this->level * 2) : ''; // Indent
145         $tag = $this->casefolding ? strtoupper($tag) : $tag; // Follow casefolding
146         $end = $this->nullcontent ? ' /' : ''; // Tag without content, close it
148         // Build attributes output
149         $attrstring = '';
150         if (!empty($attributes) && is_array($attributes)) {
151             // TODO: check the attr name is valid
152             foreach ($attributes as $name => $value) {
153                 $name = $this->casefolding ? strtoupper($name) : $name; // Follow casefolding
154                 $attrstring .= ' ' . $name . '="'.
155                     $this->xml_safe_attr_content($value) . '"';
156             }
157         }
159         // Optional xml schema definition (level 0 only)
160         $schemastring = '';
161         if ($this->level == 0 && !empty($this->xmlschema)) {
162             $schemastring .= "\n    " . 'xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"' .
163                              "\n    " . 'xsi:noNamespaceSchemaLocation="' . $this->xml_safe_attr_content($this->xmlschema) . '"';
164         }
166         // Send to xml_output
167         $this->write($pre . '<' . $tag . $attrstring . $schemastring . $end . '>');
169         // Acumulate the tag and inc level
170         if (!$this->nullcontent) {
171             array_push($this->opentags, $tag);
172             $this->level++;
173         }
174         $this->lastwastext = false;
175     }
177     /**
178      * Outputs one XML end tag
179      */
180     public function end_tag($tag) {
181         // TODO: check the tag name is valid
183         if ($this->level == 0) { // Nothing to end, already at level 0
184             throw new xml_writer_exception('xml_writer_end_tag_no_match');
185         }
187         $pre = $this->lastwastext ? '' : "\n" . str_repeat(' ', ($this->level - 1) * 2); // Indent
188         $tag = $this->casefolding ? strtoupper($tag) : $tag; // Follow casefolding
190         $lastopentag = array_pop($this->opentags);
192         if ($tag != $lastopentag) {
193             $a = new stdclass();
194             $a->lastopen = $lastopentag;
195             $a->tag = $tag;
196             throw new xml_writer_exception('xml_writer_end_tag_no_match', $a);
197         }
199         // Send to xml_output
200         $this->write($pre . '</' . $tag . '>');
202         $this->level--;
203         $this->lastwastext = false;
204     }
207     /**
208      * Outputs one tag completely (open, contents and close)
209      */
210     public function full_tag($tag, $content = null, $attributes = null) {
211         $content = $this->text_content($content); // First of all, apply transformations
212         $this->nullcontent = is_null($content) ? true : false; // Is it null content
213         $this->begin_tag($tag, $attributes);
214         if (!$this->nullcontent) {
215             $this->write($content);
216             $this->lastwastext = true;
217             $this->end_tag($tag);
218         }
219     }
222 // Protected API starts here
224     /**
225      * Send some XML formatted chunk to output.
226      */
227     protected function write($output) {
228         $this->output->write($output);
229     }
231     /**
232      * Get default prologue contents for this writer if there isn't a custom one
233      */
234     protected function get_default_prologue() {
235         return "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n";
236     }
238     /**
239      * Clean attribute content and encode needed chars
240      * (&, <, >, ") - single quotes not needed in this class
241      * as far as we are enclosing with "
242      */
243     protected function xml_safe_attr_content($content) {
244         return htmlspecialchars($this->xml_safe_utf8($content), ENT_COMPAT);
245     }
247     /**
248      * Clean text content and encode needed chars
249      * (&, <, >)
250      */
251     protected function xml_safe_text_content($content) {
252         return htmlspecialchars($this->xml_safe_utf8($content), ENT_NOQUOTES);
253     }
255     /**
256      * Perform some UTF-8 cleaning, stripping the control chars (\x0-\x1f)
257      * but tabs (\x9), newlines (\xa) and returns (\xd). The delete control
258      * char (\x7f) is also included. All them are forbiden in XML 1.0 specs.
259      * The expression below seems to be UTF-8 safe too because it simply
260      * ignores the rest of characters. Also normalize linefeeds and return chars.
261      */
262     protected function xml_safe_utf8($content) {
263         $content = preg_replace('/[\x-\x8\xb-\xc\xe-\x1f\x7f]/is','', $content); // clean CTRL chars
264         $content = preg_replace("/\r\n|\r/", "\n", $content); // Normalize line&return=>line
265         return $content;
266     }
268     /**
269      * Returns text contents processed by the corresponding @xml_contenttransformer
270      */
271     protected function text_content($content) {
272         if (!is_null($this->contenttransformer)) { // Apply content transformation
273             $content = $this->contenttransformer->process($content);
274         }
275         return is_null($content) ? null : $this->xml_safe_text_content($content); // Safe UTF-8 and encode
276     }
279 /*
280  * Exception class used by all the @xml_writer stuff
281  */
282 class xml_writer_exception extends moodle_exception {
284     public function __construct($errorcode, $a=NULL, $debuginfo=null) {
285         parent::__construct($errorcode, 'error', '', $a, $debuginfo);
286     }