diff --git a/backup/util/xml/parser/processors/findpaths_parser_processor.class.php b/backup/util/xml/parser/processors/findpaths_parser_processor.class.php
new file mode 100644
index 00000000000..9b59821e040
--- /dev/null
+++ b/backup/util/xml/parser/processors/findpaths_parser_processor.class.php
@@ -0,0 +1,60 @@
+.
+
+/**
+ * @package moodlecore
+ * @subpackage xml
+ * @copyright 2003 onwards Eloy Lafuente (stronk7) {@link http://stronk7.com}
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+require_once($CFG->dirroot.'/backup/util/xml/parser/processors/progressive_parser_processor.class.php');
+
+/**
+ * Find paths progressive_parser_processor that will search for all the paths present in
+ * the chunks being returned. Useful to know the overal structure of the XML file.
+ */
+class findpaths_parser_processor extends progressive_parser_processor {
+
+ protected $foundpaths; // array of paths foudn in the chunks received from the parser
+
+ public function __construct() {
+ parent::__construct();
+ $this->foundpaths = array();
+ }
+
+ public function process_chunk($data) {
+ if (isset($data['tags'])) {
+ foreach ($data['tags'] as $tag) {
+ $tagpath = $data['path'] . '/' . $tag['name'];
+ if (!array_key_exists($tagpath, $this->foundpaths)) {
+ $this->foundpaths[$tagpath] = 1;
+ } else {
+ $this->foundpaths[$tagpath]++;
+ }
+ }
+ }
+ }
+
+ public function debug_info() {
+ $debug = array();
+ foreach($this->foundpaths as $path => $chunks) {
+ $debug['paths'][$path] = $chunks;
+ }
+ return array_merge($debug, parent::debug_info());
+ }
+}
diff --git a/backup/util/xml/parser/processors/null_parser_processor.class.php b/backup/util/xml/parser/processors/null_parser_processor.class.php
new file mode 100644
index 00000000000..cbaeab77dc3
--- /dev/null
+++ b/backup/util/xml/parser/processors/null_parser_processor.class.php
@@ -0,0 +1,35 @@
+.
+
+/**
+ * @package moodlecore
+ * @subpackage xml
+ * @copyright 2003 onwards Eloy Lafuente (stronk7) {@link http://stronk7.com}
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+require_once($CFG->dirroot.'/backup/util/xml/parser/processors/progressive_parser_processor.class.php');
+
+/**
+ * Null progressive_parser_processor that won't process chunks at all.
+ * Useful for comparing memory use/execution time.
+ */
+class null_parser_processor extends progressive_parser_processor {
+
+ public function process_chunk($data) {
+ }
+}
diff --git a/backup/util/xml/parser/processors/progressive_parser_processor.class.php b/backup/util/xml/parser/processors/progressive_parser_processor.class.php
new file mode 100644
index 00000000000..922c1099e52
--- /dev/null
+++ b/backup/util/xml/parser/processors/progressive_parser_processor.class.php
@@ -0,0 +1,79 @@
+.
+
+/**
+ * @package moodlecore
+ * @subpackage backup-xml
+ * @copyright 2010 onwards Eloy Lafuente (stronk7) {@link http://stronk7.com}
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+/**
+ * This abstract class implements one progressive_parser_processor
+ *
+ * Processor that will receive chunks of data from the @progressive_parser
+ * and will perform all sort of operations with them (join, split, invoke
+ * other methods, output, whatever...
+ *
+ * You will need to extend this class to get the expected functionality
+ * by implementing the @process_chunk() method to handle different
+ * chunks of information and, optionally, the @process_cdata() to
+ * process each cdata piece individually before being "published" to
+ * the chunk processor.
+ *
+ * The "propietary array format" that the parser publishes to the @progressive_parser_procesor
+ * is this:
+ * array (
+ * 'path' => path where the tags belong to,
+ * 'level'=> level (1-based) of the tags
+ * 'tags => array (
+ * 'name' => name of the tag,
+ * 'attrs'=> array( name of the attr => value of the attr),
+ * 'cdata => cdata of the tag
+ * )
+ * )
+ *
+ * TODO: Finish phpdocs
+ */
+abstract class progressive_parser_processor {
+
+ protected $inittime; // Initial microtime
+ protected $chunks; // Number of chunks processed
+
+ public function __construct() {
+ $this->inittime= microtime(true);
+ $this->chunks = 0;
+ }
+
+ abstract public function process_chunk($data);
+
+ public function process_cdata($cdata) {
+ return $cdata;
+ }
+
+ public function debug_info() {
+ return array('memory' => memory_get_peak_usage(true),
+ 'time' => microtime(true) - $this->inittime,
+ 'chunks' => $this->chunks);
+ }
+
+ public function receive_chunk($data) {
+ $this->chunks++;
+ $this->process_chunk($data);
+ }
+
+}
diff --git a/backup/util/xml/parser/processors/selective_exact_parser_processor.class.php b/backup/util/xml/parser/processors/selective_exact_parser_processor.class.php
new file mode 100644
index 00000000000..b26198dc8fe
--- /dev/null
+++ b/backup/util/xml/parser/processors/selective_exact_parser_processor.class.php
@@ -0,0 +1,53 @@
+.
+
+/**
+ * @package moodlecore
+ * @subpackage xml
+ * @copyright 2003 onwards Eloy Lafuente (stronk7) {@link http://stronk7.com}
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+require_once($CFG->dirroot.'/backup/util/xml/parser/processors/progressive_parser_processor.class.php');
+
+/**
+ * Selective progressive_parser_processor that will send chunks straight
+ * to output but only for chunks matching (in an exact way) some defined paths
+ */
+class selective_exact_parser_processor extends progressive_parser_processor {
+
+ protected $paths; // array of paths we are interested on
+
+ public function __construct(array $paths) {
+ parent::__construct();
+ $this->paths = $paths;
+ }
+
+ public function process_chunk($data) {
+ if ($this->path_is_selected($data['path'])) {
+ print_r($data); // Simply output chunk, for testing purposes
+ } else {
+ $this->chunks--; // Chunk skipped
+ }
+ }
+
+// Protected API starts here
+
+ protected function path_is_selected($path) {
+ return in_array($path, $this->paths);
+ }
+}
diff --git a/backup/util/xml/parser/processors/selective_like_parser_processor.class.php b/backup/util/xml/parser/processors/selective_like_parser_processor.class.php
new file mode 100644
index 00000000000..1948ab66206
--- /dev/null
+++ b/backup/util/xml/parser/processors/selective_like_parser_processor.class.php
@@ -0,0 +1,53 @@
+.
+
+/**
+ * @package moodlecore
+ * @subpackage xml
+ * @copyright 2003 onwards Eloy Lafuente (stronk7) {@link http://stronk7.com}
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+require_once($CFG->dirroot.'/backup/util/xml/parser/processors/progressive_parser_processor.class.php');
+
+/**
+ * Selective progressive_parser_processor that will send chunks straight
+ * to output but only for chunks matching (in a left padded way - like) some defined paths
+ */
+class selective_like_parser_processor extends progressive_parser_processor {
+
+ protected $paths; // array of paths we are interested on
+
+ public function __construct(array $paths) {
+ parent::__construct();
+ $this->paths = '=>' . implode('=>', $paths);
+ }
+
+ public function process_chunk($data) {
+ if ($this->path_is_selected($data['path'])) {
+ print_r($data); // Simply output chunk, for testing purposes
+ } else {
+ $this->chunks--; // Chunk skipped
+ }
+ }
+
+// Protected API starts here
+
+ protected function path_is_selected($path) {
+ return strpos('@=>' . $path, $this->paths);
+ }
+}
diff --git a/backup/util/xml/parser/processors/simple_parser_processor.class.php b/backup/util/xml/parser/processors/simple_parser_processor.class.php
new file mode 100644
index 00000000000..63c46bba501
--- /dev/null
+++ b/backup/util/xml/parser/processors/simple_parser_processor.class.php
@@ -0,0 +1,36 @@
+.
+
+/**
+ * @package moodlecore
+ * @subpackage xml
+ * @copyright 2003 onwards Eloy Lafuente (stronk7) {@link http://stronk7.com}
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+require_once($CFG->dirroot.'/backup/util/xml/parser/processors/progressive_parser_processor.class.php');
+
+/**
+ * Simple progressive_parser_processor that will send chunks straight
+ * to output. Useful for testing, compare memory use/execution time.
+ */
+class simple_parser_processor extends progressive_parser_processor {
+
+ public function process_chunk($data) {
+ print_r($data); // Simply output chunk, for testing purposes
+ }
+}
diff --git a/backup/util/xml/parser/processors/simplified_parser_processor.class.php b/backup/util/xml/parser/processors/simplified_parser_processor.class.php
new file mode 100644
index 00000000000..21de6b0aa54
--- /dev/null
+++ b/backup/util/xml/parser/processors/simplified_parser_processor.class.php
@@ -0,0 +1,129 @@
+.
+
+/**
+ * @package moodlecore
+ * @subpackage xml
+ * @copyright 2003 onwards Eloy Lafuente (stronk7) {@link http://stronk7.com}
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+require_once($CFG->dirroot.'/backup/util/xml/parser/processors/progressive_parser_processor.class.php');
+
+/**
+ * Abstract xml parser processor to be to simplify and dispatch parsed chunks
+ *
+ * This @progressive_parser_processor handles the requested paths,
+ * performing some conversions from the original "propietary array format"
+ * used by the @progressive_parser to a simplified structure to be used
+ * easily. Found attributes are converted automatically to tags and cdata
+ * to simpler values.
+ *
+ * Note: final tag attributes are discarded completely!
+ *
+ * TODO: Complete phpdocs
+ */
+abstract class simplified_parser_processor extends progressive_parser_processor {
+ protected $paths; // array of paths we are interested on
+ protected $parentpaths; // array of parent paths of the $paths
+ protected $parentsinfo; // array of parent attributes to be added as child tags
+
+ public function __construct(array $paths) {
+ parent::__construct();
+ $this->paths = $paths;
+ $this->parentpaths = array();
+ $this->parentsinfo = array();
+ // Add parent paths. We are looking for attributes there
+ foreach ($paths as $key => $path) {
+ $this->parentpaths[$key] = dirname($path);
+ }
+ }
+
+ /**
+ * Get the already simplified chunk and dispatch it
+ */
+ abstract public function dispatch_chunk($data);
+
+ /**
+ * Get one chunk of parsed data and make it simpler
+ * adding attributes as tags and delegating to
+ * dispatch_chunk() the procesing of the resulting chunk
+ */
+ public function process_chunk($data) {
+ // Precalculate some vars for readability
+ $path = $data['path'];
+ $parentpath = dirname($path);
+ $tag = basename($path);
+
+ // If the path is a registered parent one, store all its tags
+ // so, we'll be able to find attributes later when processing
+ // (child) registered paths (to get attributes if present)
+ if ($this->path_is_selected_parent($path)) { // if path is parent
+ if (isset($data['tags'])) { // and has tags, save them
+ $this->parentsinfo[$path] = $data['tags'];
+ }
+ }
+
+ // If the path is a registered one, let's process it
+ if ($this->path_is_selected($path)) {
+ // First of all, look for attributes available at parentsinfo
+ // in order to get them available as normal tags
+ if (isset($this->parentsinfo[$parentpath][$tag]['attrs'])) {
+ $data['tags'] = array_merge($this->parentsinfo[$parentpath][$tag]['attrs'], $data['tags']);
+ unset($this->parentsinfo[$parentpath][$tag]['attrs']);
+ }
+ // Now, let's simplify the tags array, ignoring tag attributtes and
+ // reconverting to simpler name => value array
+ foreach ($data['tags'] as $key => $value) {
+ // If the value is already a single value, do nothing
+ // surely was added above from parentsinfo
+ if (!is_array($value)) {
+ continue;
+ }
+ // If the path including the tag name matches another selected path
+ // (registered or parent) delete it, another chunk will contain that info
+ if ($this->path_is_selected($path . '/' . $key) ||
+ $this->path_is_selected_parent($path . '/' . $key)) {
+ unset($data['tags'][$key]);
+ continue;
+ }
+ // Convert to simple name => value array
+ $data['tags'][$key] = isset($value['cdata']) ? $value['cdata'] : null;
+ }
+
+ // Arrived here, if the chunk has tags, send it to dispatcher
+ if (!empty($data['tags'])) {
+ return $this->dispatch_chunk($data);
+ } else {
+ $this->chunks--; // Chunk skipped
+ }
+ } else {
+ $this->chunks--; // Chunk skipped
+ }
+ return true;
+ }
+
+// Protected API starts here
+
+ protected function path_is_selected($path) {
+ return in_array($path, $this->paths);
+ }
+
+ protected function path_is_selected_parent($path) {
+ return in_array($path, $this->parentpaths);
+ }
+}
diff --git a/backup/util/xml/parser/progressive_parser.class.php b/backup/util/xml/parser/progressive_parser.class.php
new file mode 100644
index 00000000000..d12db43495d
--- /dev/null
+++ b/backup/util/xml/parser/progressive_parser.class.php
@@ -0,0 +1,245 @@
+.
+
+/**
+ * @package moodlecore
+ * @subpackage backup-xml
+ * @copyright 2010 onwards Eloy Lafuente (stronk7) {@link http://stronk7.com}
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+/**
+ * Class implementing one SAX progressive push parser.
+ *
+ * SAX parser able to process XML content from files/variables. It supports
+ * attributes and case folding and works only with UTF-8 content. It's one
+ * progressive push parser because, intead of loading big crunchs of information
+ * in memory, it "publishes" (pushes) small information in a "propietary array format" througt
+ * the corresponding @progressive_parser_procesor, that will be the responsibe for
+ * returning information into handy formats to higher levels.
+ *
+ * Note that, while this progressive parser is able to process any XML file, it is
+ * 100% progressive so it publishes the information in the original order it's parsed (that's
+ * the expected behaviour) so information belonging to the same path can be returned in
+ * different chunks if there are inner levels/paths in the middle. Be warned!
+ *
+ * The "propietary array format" that the parser publishes to the @progressive_parser_procesor
+ * is this:
+ * array (
+ * 'path' => path where the tags belong to,
+ * 'level'=> level (1-based) of the tags
+ * 'tags => array (
+ * 'name' => name of the tag,
+ * 'attrs'=> array( name of the attr => value of the attr),
+ * 'cdata => cdata of the tag
+ * )
+ * )
+ *
+ * TODO: Finish phpdocs
+ */
+class progressive_parser {
+
+ protected $xml_parser; // PHP's low level XML SAX parser
+ protected $file; // full path to file being progressively parsed | => mutually exclusive
+ protected $contents; // contents being progressively parsed |
+ protected $procesor; // progressive_parser_procesor to be used to publish processed information
+
+ protected $level; // level of the current tag
+ protected $path; // path of the current tag
+ protected $accum; // accumulated char data of the current tag
+ protected $attrs; // attributes of the current tag
+
+ protected $topush; // array containing current level information being parsed to be "pushed"
+ protected $prevlevel; // level of the previous tag processed - to detect pushing places
+ protected $currtag; // name/value/attributes of the tag being processed
+
+ public function __construct($case_folding = false) {
+ $this->xml_parser = xml_parser_create('UTF-8');
+ xml_parser_set_option($this->xml_parser, XML_OPTION_CASE_FOLDING, $case_folding);
+ xml_set_object($this->xml_parser, $this);
+ xml_set_element_handler($this->xml_parser, array($this, 'start_tag'), array($this, 'end_tag'));
+ xml_set_character_data_handler($this->xml_parser, array($this, 'char_data'));
+
+ $this->file = null;
+ $this->contents = null;
+ $this->procesor = null;
+ $this->level = 0;
+ $this->path = '';
+ $this->accum = '';
+ $this->attrs = array();
+ $this->topush = array();
+ $this->prevlevel = 0;
+ $this->currtag = array();
+ }
+
+ /*
+ * Sets the XML file to be processed by the parser
+ */
+ public function set_file($file) {
+ if (!file_exists($file) || (!is_readable($file))) {
+ throw new progressive_parser_exception('invalid_file_to_parse');
+ }
+ $this->file = $file;
+ $this->contents = null;
+ }
+
+ /*
+ * Sets the XML contents to be processed by the parser
+ */
+ public function set_contents($contents) {
+ if (empty($contents)) {
+ throw new progressive_parser_exception('invalid_contents_to_parse');
+ }
+ $this->contents = $contents;
+ $this->file = null;
+ }
+
+ /*
+ * Define the @progressive_parser_processor in charge of processing the parsed chunks
+ */
+ public function set_processor($processor) {
+ if (!$processor instanceof progressive_parser_processor) {
+ throw new progressive_parser_exception('invalid_parser_processor');
+ }
+ $this->processor = $processor;
+ }
+
+ /*
+ * Process the XML, delegating found chunks to the @progressive_parser_processor
+ */
+ public function process() {
+ if (empty($this->processor)) {
+ throw new progressive_parser_exception('undefined_parser_processor');
+ }
+ if (empty($this->file) && empty($this->contents)) {
+ throw new progressive_parser_exception('undefined_xml_to_parse');
+ }
+ if (is_null($this->xml_parser)) {
+ throw new progressive_parser_exception('progressive_parser_already_used');
+ }
+ if ($this->file) {
+ $fh = fopen($this->file, 'r');
+ while ($buffer = fread($fh, 8192)) {
+ $this->parse($buffer, feof($fh));
+ }
+ fclose($fh);
+ } else {
+ $this->parse($this->contents, true);
+ }
+ xml_parser_free($this->xml_parser);
+ $this->xml_parser = null;
+ }
+
+// Protected API starts here
+
+ protected function parse($data, $eof) {
+ if (!xml_parse($this->xml_parser, $data, $eof)) {
+ throw new progressive_parser_exception(
+ 'xml_parsing_error', null,
+ sprintf('XML error: %s at line %d, column %d',
+ xml_error_string(xml_get_error_code($this->xml_parser)),
+ xml_get_current_line_number($this->xml_parser),
+ xml_get_current_column_number($this->xml_parser)));
+ }
+ }
+
+ protected function publish($data) {
+ $this->processor->receive_chunk($data);
+ }
+
+ protected function postprocess_cdata($data) {
+ return $this->processor->process_cdata($data);
+ }
+
+ protected function start_tag($parser, $tag, $attributes) {
+
+ // Normal update of parser internals
+ $this->level++;
+ $this->path .= '/' . $tag;
+ $this->accum = '';
+ $this->attrs = !empty($attributes) ? $attributes : array();
+
+ // Entering a new inner level, publish all the information available
+ if ($this->level > $this->prevlevel) {
+ if (!empty($this->currtag) && (!empty($this->currtag['attrs']) || !empty($this->currtag['cdata']))) {
+ $this->topush['tags'][$this->currtag['name']] = $this->currtag;
+ }
+ if (!empty($this->topush['tags'])) {
+ $this->publish($this->topush);
+ }
+ $this->currtag = array();
+ $this->topush = array();
+ }
+
+ // If not set, build to push common header
+ if (empty($this->topush)) {
+ $this->topush['path'] = dirname($this->path);
+ $this->topush['level'] = $this->level;
+ $this->topush['tags'] = array();
+ }
+
+ // Handling a new tag, create it
+ $this->currtag['name'] = $tag;
+ // And add attributes if present
+ if ($this->attrs) {
+ $this->currtag['attrs'] = $this->attrs;
+ }
+
+ // For the records
+ $this->prevlevel = $this->level;
+ }
+
+ protected function end_tag($parser, $tag) {
+
+ // Ending rencently started tag, add value to current tag
+ if ($this->level == $this->prevlevel) {
+ $this->currtag['cdata'] = $this->postprocess_cdata($this->accum);
+ $this->topush['tags'][$this->currtag['name']] = $this->currtag;
+ $this->currtag = array();
+ }
+
+ // Leaving one level, publish all the information available
+ if ($this->level < $this->prevlevel) {
+ if (!empty($this->topush['tags'])) {
+ $this->publish($this->topush);
+ }
+ $this->currtag = array();
+ $this->topush = array();
+ }
+
+ // For the records
+ $this->prevlevel = $this->level;
+
+ // Normal update of parser internals
+ $this->level--;
+ $this->path = dirname($this->path);
+ }
+
+ protected function char_data($parser, $data) {
+ $this->accum .= $data;
+ }
+}
+
+/*
+ * Exception class used by all the @progressive_parser stuff
+ */
+class progressive_parser_exception extends moodle_exception {
+
+ public function __construct($errorcode, $a=NULL, $debuginfo=null) {
+ parent::__construct($errorcode, 'error', '', $a, null, $debuginfo);
+ }
+}
diff --git a/backup/util/xml/parser/simpletest/fixtures/test1.xml b/backup/util/xml/parser/simpletest/fixtures/test1.xml
new file mode 100644
index 00000000000..f763f8554e7
--- /dev/null
+++ b/backup/util/xml/parser/simpletest/fixtures/test1.xml
@@ -0,0 +1,5 @@
+
+
One simple glossary to test backup & restore. Here it\'s the standard image:
'. + "\n". + '![]()