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.
   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 xml
  21   * @copyright 2003 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  require_once($CFG->dirroot.'/backup/util/xml/parser/processors/progressive_parser_processor.class.php');
  26  
  27  /**
  28   * Abstract xml parser processor to to simplify and dispatch parsed chunks
  29   *
  30   * This @progressive_parser_processor handles the requested paths,
  31   * performing some conversions from the original "propietary array format"
  32   * used by the @progressive_parser to a simplified structure to be used
  33   * easily. Found attributes are converted automatically to tags and cdata
  34   * to simpler values.
  35   *
  36   * Note: final tag attributes are discarded completely!
  37   *
  38   * TODO: Complete phpdocs
  39   */
  40  abstract class simplified_parser_processor extends progressive_parser_processor {
  41      protected $paths;       // array of paths we are interested on
  42      protected $parentpaths; // array of parent paths of the $paths
  43      protected $parentsinfo; // array of parent attributes to be added as child tags
  44      protected $startendinfo;// array (stack) of startend information
  45  
  46      public function __construct(array $paths = array()) {
  47          parent::__construct();
  48          $this->paths = array();
  49          $this->parentpaths = array();
  50          $this->parentsinfo = array();
  51          $this->startendinfo = array();
  52          // Add paths and parentpaths. We are looking for attributes there
  53          foreach ($paths as $key => $path) {
  54              $this->add_path($path);
  55          }
  56      }
  57  
  58      public function add_path($path) {
  59          $this->paths[] = $path;
  60          $this->parentpaths[] = progressive_parser::dirname($path);
  61      }
  62  
  63      /**
  64       * Get the already simplified chunk and dispatch it
  65       */
  66      abstract protected function dispatch_chunk($data);
  67  
  68      /**
  69       * Get one selected path and notify about start
  70       */
  71      abstract protected function notify_path_start($path);
  72  
  73      /**
  74       * Get one selected path and notify about end
  75       */
  76      abstract protected function notify_path_end($path);
  77  
  78      /**
  79       * Get one chunk of parsed data and make it simpler
  80       * adding attributes as tags and delegating to
  81       * dispatch_chunk() the procesing of the resulting chunk
  82       */
  83      public function process_chunk($data) {
  84          // Precalculate some vars for readability
  85          $path = $data['path'];
  86          $parentpath = progressive_parser::dirname($path);
  87          $tag = basename($path);
  88  
  89          // If the path is a registered parent one, store all its tags
  90          // so, we'll be able to find attributes later when processing
  91          // (child) registered paths (to get attributes if present)
  92          if ($this->path_is_selected_parent($path)) { // if path is parent
  93              if (isset($data['tags'])) {              // and has tags, save them
  94                  $this->parentsinfo[$path] = $data['tags'];
  95              }
  96          }
  97  
  98          // If the path is a registered one, let's process it
  99          if ($this->path_is_selected($path)) {
 100  
 101              // Send all the pending notify_path_start/end() notifications
 102              $this->process_pending_startend_notifications($path, 'start');
 103  
 104              // First of all, look for attributes available at parentsinfo
 105              // in order to get them available as normal tags
 106              if (isset($this->parentsinfo[$parentpath][$tag]['attrs'])) {
 107                  $data['tags'] = array_merge($this->parentsinfo[$parentpath][$tag]['attrs'], $data['tags']);
 108                  unset($this->parentsinfo[$parentpath][$tag]['attrs']);
 109              }
 110              // Now, let's simplify the tags array, ignoring tag attributtes and
 111              // reconverting to simpler name => value array. At the same time,
 112              // check for all the tag values being whitespace-string values, if all them
 113              // are whitespace strings, we aren't going to postprocess/dispatch the chunk
 114              $alltagswhitespace = true;
 115              foreach ($data['tags'] as $key => $value) {
 116                  // If the value is already a single value, do nothing
 117                  // surely was added above from parentsinfo attributes,
 118                  // so we'll process the chunk always
 119                  if (!is_array($value)) {
 120                      $alltagswhitespace = false;
 121                      continue;
 122                  }
 123  
 124                  // If the path including the tag name matches another selected path
 125                  // (registered or parent) and is null or begins with linefeed, we know it's part
 126                  // of another chunk, delete it, another chunk will contain that info
 127                  if ($this->path_is_selected($path . '/' . $key) ||
 128                      $this->path_is_selected_parent($path . '/' . $key)) {
 129                      if (!isset($value['cdata']) || substr($value['cdata'], 0, 1) === "\n") {
 130                          unset($data['tags'][$key]);
 131                          continue;
 132                      }
 133                  }
 134  
 135                  // Convert to simple name => value array
 136                  $data['tags'][$key] = isset($value['cdata']) ? $value['cdata'] : null;
 137  
 138                  // Check $alltagswhitespace continues being true
 139                  if ($alltagswhitespace && strlen($data['tags'][$key]) !== 0 && trim($data['tags'][$key]) !== '') {
 140                      $alltagswhitespace = false; // Found non-whitespace value
 141                  }
 142              }
 143  
 144              // Arrived here, if the chunk has tags and not all tags are whitespace,
 145              // send it to postprocess filter that will decide about dispatching. Else
 146              // skip the chunk completely
 147              if (!empty($data['tags']) && !$alltagswhitespace) {
 148                  return $this->postprocess_chunk($data);
 149              } else {
 150                  $this->chunks--; // Chunk skipped
 151              }
 152          } else {
 153              $this->chunks--; // Chunk skipped
 154          }
 155  
 156          return true;
 157      }
 158  
 159      /**
 160       * The parser fires this each time one path is going to be parsed
 161       *
 162       * @param string $path xml path which parsing has started
 163       */
 164      public function before_path($path) {
 165          if ($this->path_is_selected($path)) {
 166              $this->startendinfo[] = array('path' => $path, 'action' => 'start');
 167          }
 168      }
 169  
 170      /**
 171       * The parser fires this each time one path has been parsed
 172       *
 173       * @param string $path xml path which parsing has ended
 174       */
 175      public function after_path($path) {
 176          $toprocess = false;
 177          // If the path being closed matches (same or parent) the first path in the stack
 178          // we process pending startend notifications until one matching end is found
 179          if ($element = reset($this->startendinfo)) {
 180              $elepath = $element['path'];
 181              $eleaction = $element['action'];
 182              if (strpos($elepath, $path) === 0) {
 183                  $toprocess = true;
 184              }
 185  
 186          // Also, if the stack of startend notifications is empty, we can process current end
 187          // path safely
 188          } else {
 189              $toprocess = true;
 190          }
 191          if ($this->path_is_selected($path)) {
 192              $this->startendinfo[] = array('path' => $path, 'action' => 'end');
 193          }
 194          // Send all the pending startend notifications if decided to do so
 195          if ($toprocess) {
 196              $this->process_pending_startend_notifications($path, 'end');
 197          }
 198      }
 199  
 200  
 201  // Protected API starts here
 202  
 203      /**
 204       * Adjust start/end til finding one match start/end path (included)
 205       *
 206       * This will trigger all the pending {@see notify_path_start} and
 207       * {@see notify_path_end} calls for one given path and action
 208       *
 209       * @param string path the path to look for as limit
 210       * @param string action the action to look for as limit
 211       */
 212      protected function process_pending_startend_notifications($path, $action) {
 213  
 214          // Iterate until one matching path and action is found (or the array is empty)
 215          $elecount = count($this->startendinfo);
 216          $elematch = false;
 217          while ($elecount > 0 && !$elematch) {
 218              $element = array_shift($this->startendinfo);
 219              $elecount--;
 220              $elepath = $element['path'];
 221              $eleaction = $element['action'];
 222  
 223              if ($elepath == $path && $eleaction == $action) {
 224                  $elematch = true;
 225              }
 226  
 227              if ($eleaction == 'start') {
 228                  $this->notify_path_start($elepath);
 229              } else {
 230                  $this->notify_path_end($elepath);
 231              }
 232          }
 233      }
 234  
 235      protected function postprocess_chunk($data) {
 236          $this->dispatch_chunk($data);
 237      }
 238  
 239      protected function path_is_selected($path) {
 240          return in_array($path, $this->paths);
 241      }
 242  
 243      protected function path_is_selected_parent($path) {
 244          return in_array($path, $this->parentpaths);
 245      }
 246  
 247      /**
 248       * Returns the first selected parent if available or false
 249       */
 250      protected function selected_parent_exists($path) {
 251          $parentpath = progressive_parser::dirname($path);
 252          while ($parentpath != '/') {
 253              if ($this->path_is_selected($parentpath)) {
 254                  return $parentpath;
 255              }
 256              $parentpath = progressive_parser::dirname($parentpath);
 257          }
 258          return false;
 259      }
 260  }