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 }
title
Description
Body
title
Description
Body
title
Description
Body
title
Body