Search moodle.org's
Developer Documentation

See Release Notes
Long Term Support Release

  • Bug fixes for general core bugs in 4.1.x will end 13 November 2023 (12 months).
  • Bug fixes for security issues in 4.1.x will end 10 November 2025 (36 months).
  • PHP version: minimum PHP 7.4.0 Note: minimum PHP version has increased since Moodle 4.0. PHP 8.0.x is supported too.

Differences Between: [Versions 310 and 401] [Versions 311 and 401] [Versions 39 and 401] [Versions 400 and 401]

   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  }