Proyectos de Subversion Moodle

Rev

| Ultima modificación | Ver Log |

Rev Autor Línea Nro. Línea
1 efrain 1
<?php
2
 
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/>.
17
 
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
 */
24
 
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 {
41
 
42
    protected $output;     // @xml_output that defines how to output XML
43
    protected $contenttransformer; // @xml_contenttransformer to modify contents before output
44
 
45
    protected $prologue;   // Complete string prologue we want to use
46
    protected $xmlschema;  // URI to nonamespaceschema to be added to main tag
47
 
48
    protected $casefolding; // To define if xml tags must be uppercase (true) or not (false)
49
 
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
54
 
55
    protected $running; // To know if writer is running
56
 
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
        }
64
 
65
        $this->output = $output;
66
        $this->contenttransformer = $contenttransformer;
67
 
68
        $this->prologue  = null;
69
        $this->xmlschema = null;
70
 
71
        $this->casefolding = $casefolding;
72
 
73
        $this->level    = 0;
74
        $this->opentags = array();
75
        $this->lastwastext = false;
76
        $this->nullcontent = false;
77
 
78
        $this->running = null;
79
    }
80
 
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
    }
100
 
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
    }
118
 
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
    }
128
 
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
    }
138
 
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
147
 
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
        }
158
 
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
        }
165
 
166
        // Send to xml_output
167
        $this->write($pre . '<' . $tag . $attrstring . $schemastring . $end . '>');
168
 
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
    }
176
 
177
    /**
178
     * Outputs one XML end tag
179
     */
180
    public function end_tag($tag) {
181
        // TODO: check the tag name is valid
182
 
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
        }
186
 
187
        $pre = $this->lastwastext ? '' : "\n" . str_repeat(' ', ($this->level - 1) * 2); // Indent
188
        $tag = $this->casefolding ? strtoupper($tag) : $tag; // Follow casefolding
189
 
190
        $lastopentag = array_pop($this->opentags);
191
 
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
        }
198
 
199
        // Send to xml_output
200
        $this->write($pre . '</' . $tag . '>');
201
 
202
        $this->level--;
203
        $this->lastwastext = false;
204
    }
205
 
206
 
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
    }
220
 
221
 
222
// Protected API starts here
223
 
224
    /**
225
     * Send some XML formatted chunk to output.
226
     */
227
    protected function write($output) {
228
        $this->output->write($output);
229
    }
230
 
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
    }
237
 
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
    }
246
 
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
    }
254
 
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 fix_utf8($content);
266
    }
267
 
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
    }
277
}
278
 
279
/*
280
 * Exception class used by all the @xml_writer stuff
281
 */
282
class xml_writer_exception extends moodle_exception {
283
 
284
    public function __construct($errorcode, $a=NULL, $debuginfo=null) {
285
        parent::__construct($errorcode, 'error', '', $a, $debuginfo);
286
    }
287
}