Search moodle.org's
Developer Documentation

See Release Notes

  • Bug fixes for general core bugs in 3.10.x will end 8 November 2021 (12 months).
  • Bug fixes for security issues in 3.10.x will end 9 May 2022 (18 months).
  • PHP version: minimum PHP 7.2.0 Note: minimum PHP version has increased since Moodle 3.8. PHP 7.3.x and 7.4.x are supported too.

Differences Between: [Versions 310 and 401] [Versions 310 and 402] [Versions 310 and 403]

   1  <?php
   2  /**
   3   * Copyright 2012-2017 Horde LLC (http://www.horde.org/)
   4   *
   5   * See the enclosed file LICENSE for license information (LGPL). If you
   6   * did not receive this file, see http://www.horde.org/licenses/lgpl21.
   7   *
   8   * @category  Horde
   9   * @copyright 2012-2017 Horde LLC
  10   * @license   http://www.horde.org/licenses/lgpl21 LGPL 2.1
  11   * @package   Stream
  12   */
  13  
  14  /**
  15   * Object that adds convenience/utility methods to interacting with PHP
  16   * streams.
  17   *
  18   * @author    Michael Slusarz <slusarz@horde.org>
  19   * @category  Horde
  20   * @copyright 2012-2017 Horde LLC
  21   * @license   http://www.horde.org/licenses/lgpl21 LGPL 2.1
  22   * @package   Stream
  23   *
  24   * @property boolean $utf8_char  Parse character as UTF-8 data instead of
  25   *                               single byte (@since 1.4.0).
  26   */
  27  class Horde_Stream implements Serializable
  28  {
  29      /**
  30       * Stream resource.
  31       *
  32       * @var resource
  33       */
  34      public $stream;
  35  
  36      /**
  37       * Configuration parameters.
  38       *
  39       * @var array
  40       */
  41      protected $_params;
  42  
  43      /**
  44       * Parse character as UTF-8 data instead of single byte.
  45       *
  46       * @var boolean
  47       */
  48      protected $_utf8_char = false;
  49  
  50      /**
  51       * Constructor.
  52       *
  53       * @param array $opts  Configuration options.
  54       */
  55      public function __construct(array $opts = array())
  56      {
  57          $this->_params = $opts;
  58          $this->_init();
  59      }
  60  
  61      /**
  62       * Initialization method.
  63       */
  64      protected function _init()
  65      {
  66          // Sane default: read-write, 0-length stream.
  67          if (!$this->stream) {
  68              $this->stream = @fopen('php://temp', 'r+');
  69          }
  70      }
  71  
  72      /**
  73       */
  74      public function __get($name)
  75      {
  76          switch ($name) {
  77          case 'utf8_char':
  78              return $this->_utf8_char;
  79          }
  80      }
  81  
  82      /**
  83       */
  84      public function __set($name, $value)
  85      {
  86          switch ($name) {
  87          case 'utf8_char':
  88              $this->_utf8_char = (bool)$value;
  89              break;
  90          }
  91      }
  92  
  93      /**
  94       */
  95      public function __clone()
  96      {
  97          $data = strval($this);
  98          $this->stream = null;
  99          $this->_init();
 100          $this->add($data);
 101      }
 102  
 103      /**
 104       * String representation of object.
 105       *
 106       * @since 1.1.0
 107       *
 108       * @return string  The full stream converted to a string.
 109       */
 110      public function __toString()
 111      {
 112          $this->rewind();
 113          return $this->substring();
 114      }
 115  
 116      /**
 117       * Adds data to the stream.
 118       *
 119       * @param mixed $data     Data to add to the stream. Can be a resource,
 120       *                        Horde_Stream object, or a string(-ish) value.
 121       * @param boolean $reset  Reset stream pointer to initial position after
 122       *                        adding?
 123       */
 124      public function add($data, $reset = false)
 125      {
 126          if ($reset) {
 127              $pos = $this->pos();
 128          }
 129  
 130          if (is_resource($data)) {
 131              $dpos = ftell($data);
 132              while (!feof($data)) {
 133                  $this->add(fread($data, 8192));
 134              }
 135              fseek($data, $dpos);
 136          } elseif ($data instanceof Horde_Stream) {
 137              $dpos = $data->pos();
 138              while (!$data->eof()) {
 139                  $this->add($data->substring(0, 65536));
 140              }
 141              $data->seek($dpos, false);
 142          } else {
 143              fwrite($this->stream, $data);
 144          }
 145  
 146          if ($reset) {
 147              $this->seek($pos, false);
 148          }
 149      }
 150  
 151      /**
 152       * Returns the length of the data. Does not change the stream position.
 153       *
 154       * @param boolean $utf8  If true, determines the UTF-8 length of the
 155       *                       stream (as of 1.4.0). If false, determines the
 156       *                       byte length of the stream.
 157       *
 158       * @return integer  Stream size.
 159       *
 160       * @throws Horde_Stream_Exception
 161       */
 162      public function length($utf8 = false)
 163      {
 164          $pos = $this->pos();
 165  
 166          if ($utf8 && $this->_utf8_char) {
 167              $this->rewind();
 168              $len = 0;
 169              while ($this->getChar() !== false) {
 170                  ++$len;
 171              }
 172          } elseif (!$this->end()) {
 173              throw new Horde_Stream_Exception('ERROR');
 174          } else {
 175              $len = $this->pos();
 176          }
 177  
 178          if (!$this->seek($pos, false)) {
 179              throw new Horde_Stream_Exception('ERROR');
 180          }
 181  
 182          return $len;
 183      }
 184  
 185      /**
 186       * Get a string up to a certain character (or EOF).
 187       *
 188       * @param string $end   The character to stop reading at. As of 1.4.0,
 189       *                      $char can be a multi-character UTF-8 string.
 190       * @param boolean $all  If true, strips all repetitions of $end from
 191       *                      the end. If false, stops at the first instance
 192       *                      of $end. (@since 1.5.0)
 193       *
 194       * @return string  The string up to $end (stream is positioned after the
 195       *                 end character(s), all of which are stripped from the
 196       *                 return data).
 197       */
 198      public function getToChar($end, $all = true)
 199      {
 200          if (($len = strlen($end)) === 1) {
 201              $out = '';
 202              do {
 203                  if (($tmp = stream_get_line($this->stream, 8192, $end)) === false) {
 204                      return $out;
 205                  }
 206  
 207                  $out .= $tmp;
 208                  if ((strlen($tmp) < 8192) || ($this->peek(-1) == $end)) {
 209                      break;
 210                  }
 211              } while (true);
 212          } else {
 213              $res = $this->search($end);
 214  
 215              if (is_null($res)) {
 216                  return $this->substring();
 217              }
 218  
 219              $out = substr($this->getString(null, $res + $len - 1), 0, $len * -1);
 220          }
 221  
 222          /* Remove all further characters also. */
 223          if ($all) {
 224              while ($this->peek($len) == $end) {
 225                  $this->seek($len);
 226              }
 227          }
 228  
 229          return $out;
 230      }
 231  
 232      /**
 233       * Return the current character(s) without moving the pointer.
 234       *
 235       * @param integer $length  The peek length (since 1.4.0).
 236       *
 237       * @return string  The current character.
 238       */
 239      public function peek($length = 1)
 240      {
 241          $out = '';
 242  
 243          for ($i = 0; $i < $length; ++$i) {
 244              if (($c = $this->getChar()) === false) {
 245                  break;
 246              }
 247              $out .= $c;
 248          }
 249  
 250          $this->seek(strlen($out) * -1);
 251  
 252          return $out;
 253      }
 254  
 255      /**
 256       * Search for character(s) and return its position.
 257       *
 258       * @param string $char      The character to search for. As of 1.4.0,
 259       *                          $char can be a multi-character UTF-8 string.
 260       * @param boolean $reverse  Do a reverse search?
 261       * @param boolean $reset    Reset the pointer to the original position?
 262       *
 263       * @return mixed  The start position of the search string (integer), or
 264       *                null if character not found.
 265       */
 266      public function search($char, $reverse = false, $reset = true)
 267      {
 268          $found_pos = null;
 269  
 270          if ($len = strlen($char)) {
 271              $pos = $this->pos();
 272              $single_char = ($len === 1);
 273  
 274              do {
 275                  if ($reverse) {
 276                      for ($i = $pos - 1; $i >= 0; --$i) {
 277                          $this->seek($i, false);
 278                          $c = $this->peek();
 279                          if ($c == ($single_char ? $char : substr($char, 0, strlen($c)))) {
 280                              $found_pos = $i;
 281                              break;
 282                          }
 283                      }
 284                  } else {
 285                      /* Optimization for the common use case of searching for
 286                       * a single character in byte data. Reduces calling
 287                       * getChar() a bunch of times. */
 288                      $fgetc = ($single_char && !$this->_utf8_char);
 289  
 290                      while (($c = ($fgetc ? fgetc($this->stream) : $this->getChar())) !== false) {
 291                          if ($c == ($single_char ? $char : substr($char, 0, strlen($c)))) {
 292                              $found_pos = $this->pos() - ($single_char ? 1 : strlen($c));
 293                              break;
 294                          }
 295                      }
 296                  }
 297  
 298                  if ($single_char ||
 299                      is_null($found_pos) ||
 300                      ($this->getString($found_pos, $found_pos + $len - 1) == $char)) {
 301                      break;
 302                  }
 303  
 304                  $this->seek($found_pos + ($reverse ? 0 : 1), false);
 305                  $found_pos = null;
 306              } while (true);
 307  
 308              $this->seek(
 309                  ($reset || is_null($found_pos)) ? $pos : $found_pos,
 310                  false
 311              );
 312          }
 313  
 314          return $found_pos;
 315      }
 316  
 317      /**
 318       * Returns the stream (or a portion of it) as a string. Position values
 319       * are the byte position in the stream.
 320       *
 321       * @param integer $start  The starting position. If positive, start from
 322       *                        this position. If negative, starts this length
 323       *                        back from the current position. If null, starts
 324       *                        from the current position.
 325       * @param integer $end    The ending position relative to the beginning of
 326       *                        the stream (if positive). If negative, end this
 327       *                        length back from the end of the stream. If null,
 328       *                        reads to the end of the stream.
 329       *
 330       * @return string  A string.
 331       */
 332      public function getString($start = null, $end = null)
 333      {
 334          if (!is_null($start) && ($start >= 0)) {
 335              $this->seek($start, false);
 336              $start = 0;
 337          }
 338  
 339          if (is_null($end)) {
 340              $len = null;
 341          } else {
 342              $end = ($end >= 0)
 343                  ? $end - $this->pos() + 1
 344                  : $this->length() - $this->pos() + $end;
 345              $len = max($end, 0);
 346          }
 347  
 348          return $this->substring($start, $len);
 349      }
 350  
 351      /**
 352       * Return part of the stream as a string.
 353       *
 354       * @since 1.4.0
 355       *
 356       * @param integer $start   Start, as an offset from the current postion.
 357       * @param integer $length  Length of string to return. If null, returns
 358       *                         rest of the stream. If negative, this many
 359       *                         characters will be omitted from the end of the
 360       *                         stream.
 361       * @param boolean $char    If true, $start/$length is the length in
 362       *                         characters. If false, $start/$length is the
 363       *                         length in bytes.
 364       *
 365       * @return string  The substring.
 366       */
 367      public function substring($start = 0, $length = null, $char = false)
 368      {
 369          if ($start !== 0) {
 370              $this->seek($start, true, $char);
 371          }
 372  
 373          $out = '';
 374          $to_end = is_null($length);
 375  
 376          /* If length is greater than remaining stream, use more efficient
 377           * algorithm below. Also, if doing a negative length, deal with that
 378           * below also. */
 379          if ($char &&
 380              $this->_utf8_char &&
 381              !$to_end &&
 382              ($length >= 0) &&
 383              ($length < ($this->length() - $this->pos()))) {
 384              while ($length-- && (($char = $this->getChar()) !== false)) {
 385                  $out .= $char;
 386              }
 387              return $out;
 388          }
 389  
 390          if (!$to_end && ($length < 0)) {
 391              $pos = $this->pos();
 392              $this->end();
 393              $this->seek($length, true, $char);
 394              $length = $this->pos() - $pos;
 395              $this->seek($pos, false);
 396              if ($length < 0) {
 397                  return '';
 398              }
 399          }
 400  
 401          while (!feof($this->stream) && ($to_end || $length)) {
 402              $read = fread($this->stream, $to_end ? 16384 : $length);
 403              $out .= $read;
 404              if (!$to_end) {
 405                  $length -= strlen($read);
 406              }
 407          }
 408  
 409          return $out;
 410      }
 411  
 412      /**
 413       * Auto-determine the EOL string.
 414       *
 415       * @since 1.3.0
 416       *
 417       * @return string  The EOL string, or null if no EOL found.
 418       */
 419      public function getEOL()
 420      {
 421          $pos = $this->pos();
 422  
 423          $this->rewind();
 424          $pos2 = $this->search("\n", false, false);
 425          if ($pos2) {
 426              $this->seek(-1);
 427              $eol = ($this->getChar() == "\r")
 428                  ? "\r\n"
 429                  : "\n";
 430          } else {
 431              $eol = is_null($pos2)
 432                  ? null
 433                  : "\n";
 434          }
 435  
 436          $this->seek($pos, false);
 437  
 438          return $eol;
 439      }
 440  
 441      /**
 442       * Return a character from the string.
 443       *
 444       * @since 1.4.0
 445       *
 446       * @return string  Character (single byte, or UTF-8 character if
 447       *                 $utf8_char is true).
 448       */
 449      public function getChar()
 450      {
 451          $char = fgetc($this->stream);
 452          if (!$this->_utf8_char) {
 453              return $char;
 454          }
 455  
 456          $c = ord($char);
 457          if ($c < 0x80) {
 458              return $char;
 459          }
 460  
 461          if ($c < 0xe0) {
 462              $n = 1;
 463          } elseif ($c < 0xf0) {
 464              $n = 2;
 465          } elseif ($c < 0xf8) {
 466              $n = 3;
 467          } else {
 468              throw new Horde_Stream_Exception('ERROR');
 469          }
 470  
 471          for ($i = 0; $i < $n; ++$i) {
 472              if (($c = fgetc($this->stream)) === false) {
 473                  throw new Horde_Stream_Exception('ERROR');
 474              }
 475              $char .= $c;
 476          }
 477  
 478          return $char;
 479      }
 480  
 481      /**
 482       * Return the current stream pointer position.
 483       *
 484       * @since 1.4.0
 485       *
 486       * @return mixed  The current position (integer), or false.
 487       */
 488      public function pos()
 489      {
 490          return ftell($this->stream);
 491      }
 492  
 493      /**
 494       * Rewind the internal stream to the beginning.
 495       *
 496       * @since 1.4.0
 497       *
 498       * @return boolean  True if successful.
 499       */
 500      public function rewind()
 501      {
 502          return rewind($this->stream);
 503      }
 504  
 505      /**
 506       * Move internal pointer.
 507       *
 508       * @since 1.4.0
 509       *
 510       * @param integer $offset  The offset.
 511       * @param boolean $curr    If true, offset is from current position. If
 512       *                         false, offset is from beginning of stream.
 513       * @param boolean $char    If true, $offset is the length in characters.
 514       *                         If false, $offset is the length in bytes.
 515       *
 516       * @return boolean  True if successful.
 517       */
 518      public function seek($offset = 0, $curr = true, $char = false)
 519      {
 520          if (!$offset) {
 521              return (bool)$curr ?: $this->rewind();
 522          }
 523  
 524          if ($offset < 0) {
 525              if (!$curr) {
 526                  return true;
 527              } elseif (abs($offset) > $this->pos()) {
 528                  return $this->rewind();
 529              }
 530          }
 531  
 532          if ($char && $this->_utf8_char) {
 533              if ($offset > 0) {
 534                  if (!$curr) {
 535                      $this->rewind();
 536                  }
 537  
 538                  do {
 539                      $this->getChar();
 540                  } while (--$offset);
 541              } else {
 542                  $pos = $this->pos();
 543                  $offset = abs($offset);
 544  
 545                  while ($pos-- && $offset) {
 546                      fseek($this->stream, -1, SEEK_CUR);
 547                      if ((ord($this->peek()) & 0xC0) != 0x80) {
 548                          --$offset;
 549                      }
 550                  }
 551              }
 552  
 553              return true;
 554          }
 555  
 556          return (fseek($this->stream, $offset, $curr ? SEEK_CUR : SEEK_SET) === 0);
 557      }
 558  
 559      /**
 560       * Move internal pointer to the end of the stream.
 561       *
 562       * @since 1.4.0
 563       *
 564       * @param integer $offset  Move this offset from the end.
 565       *
 566       * @return boolean  True if successful.
 567       */
 568      public function end($offset = 0)
 569      {
 570          return (fseek($this->stream, $offset, SEEK_END) === 0);
 571      }
 572  
 573      /**
 574       * Has the end of the stream been reached?
 575       *
 576       * @since 1.4.0
 577       *
 578       * @return boolean  True if the end of the stream has been reached.
 579       */
 580      public function eof()
 581      {
 582          return feof($this->stream);
 583      }
 584  
 585      /**
 586       * Close the stream.
 587       *
 588       * @since 1.4.0
 589       */
 590      public function close()
 591      {
 592          if ($this->stream) {
 593              fclose($this->stream);
 594          }
 595      }
 596  
 597      /* Serializable methods. */
 598  
 599      /**
 600       */
 601      public function serialize()
 602      {
 603          $this->_params['_pos'] = $this->pos();
 604  
 605          return json_encode(array(
 606              strval($this),
 607              $this->_params
 608          ));
 609      }
 610  
 611      /**
 612       */
 613      public function unserialize($data)
 614      {
 615          $this->_init();
 616  
 617          $data = json_decode($data, true);
 618          $this->add($data[0]);
 619          $this->seek($data[1]['_pos'], false);
 620          unset($data[1]['_pos']);
 621          $this->_params = $data[1];
 622      }
 623  
 624  }