3 // This file is part of Moodle - http://moodle.org/
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.
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.
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/>.
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
26 * Class implementing one (more or less complete) UTF-8 XML writer
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).
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.
37 * TODO: Provide UTF-8 safe strtoupper() function if using casefolding and non-ascii tags/attrs names
38 * TODO: Finish phpdocs
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');
61 if (!is_null($contenttransformer) && !$contenttransformer instanceof xml_contenttransformer
) {
62 throw new xml_writer_exception('invalid_xml_contenttransformer');
65 $this->output
= $output;
66 $this->contenttransformer
= $contenttransformer;
68 $this->prologue
= null;
69 $this->xmlschema
= null;
71 $this->casefolding
= $casefolding;
74 $this->opentags
= array();
75 $this->lastwastext
= false;
76 $this->nullcontent
= false;
78 $this->running
= null;
82 * Initializes the XML writer, preparing it to accept instructions, also
83 * invoking the underlying @xml_output init method to be ready for operation
85 public function start() {
86 if ($this->running
=== true) {
87 throw new xml_writer_exception('xml_writer_already_started');
89 if ($this->running
=== false) {
90 throw new xml_writer_exception('xml_writer_already_stopped');
92 $this->output
->start(); // Initialize whatever we need in output
93 if (!is_null($this->prologue
)) { // Output prologue
94 $this->write($this->prologue
);
96 $this->write($this->get_default_prologue());
98 $this->running
= true;
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
105 public function stop() {
106 if (is_null($this->running
)) {
107 throw new xml_writer_exception('xml_writer_not_started');
109 if ($this->running
=== false) {
110 throw new xml_writer_exception('xml_writer_already_stopped');
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');
115 $this->output
->stop();
116 $this->running
= false;
120 * Set the URI location for the *nonamespace* schema to be used by the (whole) XML document
122 public function set_nonamespace_schema($uri) {
123 if ($this->running
) {
124 throw new xml_writer_exception('xml_writer_already_started');
126 $this->xmlschema
= $uri;
130 * Define the complete prologue to be used, replacing the simple, default one
132 public function set_prologue($prologue) {
133 if ($this->running
) {
134 throw new xml_writer_exception('xml_writer_already_started');
136 $this->prologue
= $prologue;
140 * Outputs one XML start tag with optional attributes (name => value array)
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
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) . '"';
159 // Optional xml schema definition (level 0 only)
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
) . '"';
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);
174 $this->lastwastext
= false;
178 * Outputs one XML end tag
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');
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) {
194 $a->lastopen
= $lastopentag;
196 throw new xml_writer_exception('xml_writer_end_tag_no_match', $a);
199 // Send to xml_output
200 $this->write($pre . '</' . $tag . '>');
203 $this->lastwastext
= false;
208 * Outputs one tag completely (open, contents and close)
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);
222 // Protected API starts here
225 * Send some XML formatted chunk to output.
227 protected function write($output) {
228 $this->output
->write($output);
232 * Get default prologue contents for this writer if there isn't a custom one
234 protected function get_default_prologue() {
235 return "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n";
239 * Clean attribute content and encode needed chars
240 * (&, <, >, ") - single quotes not needed in this class
241 * as far as we are enclosing with "
243 protected function xml_safe_attr_content($content) {
244 return htmlspecialchars($this->xml_safe_utf8($content), ENT_COMPAT
);
248 * Clean text content and encode needed chars
251 protected function xml_safe_text_content($content) {
252 return htmlspecialchars($this->xml_safe_utf8($content), ENT_NOQUOTES
);
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.
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
269 * Returns text contents processed by the corresponding @xml_contenttransformer
271 protected function text_content($content) {
272 if (!is_null($this->contenttransformer
)) { // Apply content transformation
273 $content = $this->contenttransformer
->process($content);
275 return is_null($content) ?
null : $this->xml_safe_text_content($content); // Safe UTF-8 and encode
280 * Exception class used by all the @xml_writer stuff
282 class xml_writer_exception
extends moodle_exception
{
284 public function __construct($errorcode, $a=NULL, $debuginfo=null) {
285 parent
::__construct($errorcode, 'error', '', $a, $debuginfo);