1<?php 2/** 3 * Part of the Joomla Framework Filesystem Package 4 * 5 * @copyright Copyright (C) 2005 - 2020 Open Source Matters, Inc. All rights reserved. 6 * @license GNU General Public License version 2 or later; see LICENSE 7 */ 8 9namespace Joomla\Filesystem; 10 11use Joomla\Filesystem\Exception\FilesystemException; 12 13/** 14 * Joomla! Stream Interface 15 * 16 * The Joomla! stream interface is designed to handle files as streams 17 * where as the legacy JFile static class treated files in a rather 18 * atomic manner. 19 * 20 * This class adheres to the stream wrapper operations: 21 * 22 * @link https://www.php.net/manual/en/function.stream-get-wrappers.php 23 * @link https://www.php.net/manual/en/intro.stream.php PHP Stream Manual 24 * @link https://www.php.net/manual/en/wrappers.php Stream Wrappers 25 * @link https://www.php.net/manual/en/filters.php Stream Filters 26 * @link https://www.php.net/manual/en/transports.php Socket Transports (used by some options, particularly HTTP proxy) 27 * @since 1.0 28 */ 29class Stream 30{ 31 /** 32 * File Mode 33 * 34 * @var integer 35 * @since 1.0 36 */ 37 protected $filemode = 0644; 38 39 /** 40 * Directory Mode 41 * 42 * @var integer 43 * @since 1.0 44 */ 45 protected $dirmode = 0755; 46 47 /** 48 * Default Chunk Size 49 * 50 * @var integer 51 * @since 1.0 52 */ 53 protected $chunksize = 8192; 54 55 /** 56 * Filename 57 * 58 * @var string 59 * @since 1.0 60 */ 61 protected $filename; 62 63 /** 64 * Prefix of the connection for writing 65 * 66 * @var string 67 * @since 1.0 68 */ 69 protected $writeprefix; 70 71 /** 72 * Prefix of the connection for reading 73 * 74 * @var string 75 * @since 1.0 76 */ 77 protected $readprefix; 78 79 /** 80 * Read Processing method 81 * 82 * @var string gz, bz, f 83 * If a scheme is detected, fopen will be defaulted 84 * To use compression with a network stream use a filter 85 * @since 1.0 86 */ 87 protected $processingmethod = 'f'; 88 89 /** 90 * Filters applied to the current stream 91 * 92 * @var array 93 * @since 1.0 94 */ 95 protected $filters = array(); 96 97 /** 98 * File Handle 99 * 100 * @var resource 101 * @since 1.0 102 */ 103 protected $fh; 104 105 /** 106 * File size 107 * 108 * @var integer 109 * @since 1.0 110 */ 111 protected $filesize; 112 113 /** 114 * Context to use when opening the connection 115 * 116 * @var string 117 * @since 1.0 118 */ 119 protected $context; 120 121 /** 122 * Context options; used to rebuild the context 123 * 124 * @var array 125 * @since 1.0 126 */ 127 protected $contextOptions; 128 129 /** 130 * The mode under which the file was opened 131 * 132 * @var string 133 * @since 1.0 134 */ 135 protected $openmode; 136 137 /** 138 * Constructor 139 * 140 * @param string $writeprefix Prefix of the stream (optional). Unlike the JPATH_*, this has a final path separator! 141 * @param string $readprefix The read prefix (optional). 142 * @param array $context The context options (optional). 143 * 144 * @since 1.0 145 */ 146 public function __construct($writeprefix = '', $readprefix = '', $context = array()) 147 { 148 $this->writeprefix = $writeprefix; 149 $this->readprefix = $readprefix; 150 $this->contextOptions = $context; 151 $this->_buildContext(); 152 } 153 154 /** 155 * Destructor 156 * 157 * @since 1.0 158 */ 159 public function __destruct() 160 { 161 // Attempt to close on destruction if there is a file handle 162 if ($this->fh) 163 { 164 @$this->close(); 165 } 166 } 167 168 /** 169 * Creates a new stream object with appropriate prefix 170 * 171 * @param boolean $usePrefix Prefix the connections for writing 172 * @param string $ua UA User agent to use 173 * @param boolean $uamask User agent masking (prefix Mozilla) 174 * 175 * @return Stream 176 * 177 * @see Stream 178 * @since 1.0 179 */ 180 public static function getStream($usePrefix = true, $ua = null, $uamask = false) 181 { 182 // Setup the context; Joomla! UA and overwrite 183 $context = array(); 184 185 // Set the UA for HTTP 186 $context['http']['user_agent'] = $ua ?: 'Joomla! Framework Stream'; 187 188 if ($usePrefix) 189 { 190 return new Stream(JPATH_ROOT . '/', JPATH_ROOT, $context); 191 } 192 193 return new Stream('', '', $context); 194 } 195 196 /** 197 * Generic File Operations 198 * 199 * Open a stream with some lazy loading smarts 200 * 201 * @param string $filename Filename 202 * @param string $mode Mode string to use 203 * @param boolean $useIncludePath Use the PHP include path 204 * @param resource $context Context to use when opening 205 * @param boolean $usePrefix Use a prefix to open the file 206 * @param boolean $relative Filename is a relative path (if false, strips JPATH_ROOT to make it relative) 207 * @param boolean $detectprocessingmode Detect the processing method for the file and use the appropriate function 208 * to handle output automatically 209 * 210 * @return boolean 211 * 212 * @since 1.0 213 * @throws FilesystemException 214 */ 215 public function open($filename, $mode = 'r', $useIncludePath = false, $context = null, $usePrefix = false, $relative = false, 216 $detectprocessingmode = false 217 ) 218 { 219 $filename = $this->_getFilename($filename, $mode, $usePrefix, $relative); 220 221 if (!$filename) 222 { 223 throw new FilesystemException('Filename not set'); 224 } 225 226 $this->filename = $filename; 227 $this->openmode = $mode; 228 229 $url = parse_url($filename); 230 231 if (isset($url['scheme'])) 232 { 233 $scheme = ucfirst($url['scheme']); 234 235 // If we're dealing with a Joomla! stream, load it 236 if (Helper::isJoomlaStream($scheme)) 237 { 238 // Map to StringWrapper if required 239 if ($scheme === 'String') 240 { 241 $scheme = 'StringWrapper'; 242 } 243 244 require_once __DIR__ . '/Stream/' . $scheme . '.php'; 245 } 246 247 // We have a scheme! force the method to be f 248 $this->processingmethod = 'f'; 249 } 250 elseif ($detectprocessingmode) 251 { 252 $ext = strtolower(pathinfo($this->filename, \PATHINFO_EXTENSION)); 253 254 switch ($ext) 255 { 256 case 'tgz': 257 case 'gz': 258 case 'gzip': 259 $this->processingmethod = 'gz'; 260 261 break; 262 263 case 'tbz2': 264 case 'bz2': 265 case 'bzip2': 266 $this->processingmethod = 'bz'; 267 268 break; 269 270 default: 271 $this->processingmethod = 'f'; 272 273 break; 274 } 275 } 276 277 // Capture PHP errors 278 $php_errormsg = 'Error Unknown whilst opening a file'; 279 $trackErrors = ini_get('track_errors'); 280 ini_set('track_errors', true); 281 282 // Decide which context to use: 283 switch ($this->processingmethod) 284 { 285 // Gzip doesn't support contexts or streams 286 case 'gz': 287 $this->fh = gzopen($filename, $mode, $useIncludePath); 288 289 break; 290 291 // Bzip2 is much like gzip except it doesn't use the include path 292 case 'bz': 293 $this->fh = bzopen($filename, $mode); 294 295 break; 296 297 // Fopen can handle streams 298 case 'f': 299 default: 300 // One supplied at open; overrides everything 301 if ($context) 302 { 303 $this->fh = @fopen($filename, $mode, $useIncludePath, $context); 304 } 305 elseif ($this->context) 306 { 307 // One provided at initialisation 308 $this->fh = @fopen($filename, $mode, $useIncludePath, $this->context); 309 } 310 else 311 { 312 // No context; all defaults 313 $this->fh = @fopen($filename, $mode, $useIncludePath); 314 } 315 316 break; 317 } 318 319 // Restore error tracking to what it was before 320 ini_set('track_errors', $trackErrors); 321 322 if (!$this->fh) 323 { 324 throw new FilesystemException($php_errormsg); 325 } 326 327 // Return the result 328 return true; 329 } 330 331 /** 332 * Attempt to close a file handle 333 * 334 * Will return false if it failed and true on success 335 * If the file is not open the system will return true, this function destroys the file handle as well 336 * 337 * @return boolean 338 * 339 * @since 1.0 340 * @throws FilesystemException 341 */ 342 public function close() 343 { 344 if (!$this->fh) 345 { 346 throw new FilesystemException('File not open'); 347 } 348 349 // Capture PHP errors 350 $php_errormsg = 'Error Unknown'; 351 $trackErrors = ini_get('track_errors'); 352 ini_set('track_errors', true); 353 354 switch ($this->processingmethod) 355 { 356 case 'gz': 357 $res = gzclose($this->fh); 358 359 break; 360 361 case 'bz': 362 $res = bzclose($this->fh); 363 364 break; 365 366 case 'f': 367 default: 368 $res = fclose($this->fh); 369 370 break; 371 } 372 373 // Restore error tracking to what it was before 374 ini_set('track_errors', $trackErrors); 375 376 if (!$res) 377 { 378 throw new FilesystemException($php_errormsg); 379 } 380 381 // Reset this 382 $this->fh = null; 383 384 // If we wrote, chmod the file after it's closed 385 if ($this->openmode[0] == 'w') 386 { 387 $this->chmod(); 388 } 389 390 // Return the result 391 return true; 392 } 393 394 /** 395 * Work out if we're at the end of the file for a stream 396 * 397 * @return boolean 398 * 399 * @since 1.0 400 * @throws FilesystemException 401 */ 402 public function eof() 403 { 404 if (!$this->fh) 405 { 406 throw new FilesystemException('File not open'); 407 } 408 409 // Capture PHP errors 410 $php_errormsg = ''; 411 $trackErrors = ini_get('track_errors'); 412 ini_set('track_errors', true); 413 414 switch ($this->processingmethod) 415 { 416 case 'gz': 417 $res = gzeof($this->fh); 418 419 break; 420 421 case 'bz': 422 case 'f': 423 default: 424 $res = feof($this->fh); 425 426 break; 427 } 428 429 // Restore error tracking to what it was before 430 ini_set('track_errors', $trackErrors); 431 432 if ($php_errormsg) 433 { 434 throw new FilesystemException($php_errormsg); 435 } 436 437 // Return the result 438 return $res; 439 } 440 441 /** 442 * Retrieve the file size of the path 443 * 444 * @return integer|boolean 445 * 446 * @since 1.0 447 * @throws FilesystemException 448 */ 449 public function filesize() 450 { 451 if (!$this->filename) 452 { 453 throw new FilesystemException('File not open'); 454 } 455 456 // Capture PHP errors 457 $php_errormsg = ''; 458 $trackErrors = ini_get('track_errors'); 459 ini_set('track_errors', true); 460 $res = @filesize($this->filename); 461 462 if (!$res) 463 { 464 $tmpError = ''; 465 466 if ($php_errormsg) 467 { 468 // Something went wrong. 469 // Store the error in case we need it. 470 $tmpError = $php_errormsg; 471 } 472 473 $res = Helper::remotefsize($this->filename); 474 475 if (!$res) 476 { 477 // Restore error tracking to what it was before. 478 ini_set('track_errors', $trackErrors); 479 480 if ($tmpError) 481 { 482 // Use the php_errormsg from before 483 throw new FilesystemException($tmpError); 484 } 485 486 // Error but nothing from php? How strange! Create our own 487 throw new FilesystemException('Failed to get file size. This may not work for all streams.'); 488 } 489 490 $this->filesize = $res; 491 $retval = $res; 492 } 493 else 494 { 495 $this->filesize = $res; 496 $retval = $res; 497 } 498 499 // Restore error tracking to what it was before. 500 ini_set('track_errors', $trackErrors); 501 502 // Return the result 503 return $retval; 504 } 505 506 /** 507 * Get a line from the stream source. 508 * 509 * @param integer $length The number of bytes (optional) to read. 510 * 511 * @return string 512 * 513 * @since 1.0 514 * @throws FilesystemException 515 */ 516 public function gets($length = 0) 517 { 518 if (!$this->fh) 519 { 520 throw new FilesystemException('File not open'); 521 } 522 523 // Capture PHP errors 524 $php_errormsg = 'Error Unknown'; 525 $trackErrors = ini_get('track_errors'); 526 ini_set('track_errors', true); 527 528 switch ($this->processingmethod) 529 { 530 case 'gz': 531 $res = $length ? gzgets($this->fh, $length) : gzgets($this->fh); 532 533 break; 534 535 case 'bz': 536 case 'f': 537 default: 538 $res = $length ? fgets($this->fh, $length) : fgets($this->fh); 539 540 break; 541 } 542 543 // Restore error tracking to what it was before 544 ini_set('track_errors', $trackErrors); 545 546 if (!$res) 547 { 548 throw new FilesystemException($php_errormsg); 549 } 550 551 // Return the result 552 return $res; 553 } 554 555 /** 556 * Read a file 557 * 558 * Handles user space streams appropriately otherwise any read will return 8192 559 * 560 * @param integer $length Length of data to read 561 * 562 * @return string 563 * 564 * @link https://www.php.net/manual/en/function.fread.php 565 * @since 1.0 566 * @throws FilesystemException 567 */ 568 public function read($length = 0) 569 { 570 if (!$this->fh) 571 { 572 throw new FilesystemException('File not open'); 573 } 574 575 if (!$this->filesize && !$length) 576 { 577 // Get the filesize 578 $this->filesize(); 579 580 if (!$this->filesize) 581 { 582 // Set it to the biggest and then wait until eof 583 $length = -1; 584 } 585 else 586 { 587 $length = $this->filesize; 588 } 589 } 590 591 $retval = false; 592 593 // Capture PHP errors 594 $php_errormsg = 'Error Unknown'; 595 $trackErrors = ini_get('track_errors'); 596 ini_set('track_errors', true); 597 $remaining = $length; 598 599 do 600 { 601 // Do chunked reads where relevant 602 switch ($this->processingmethod) 603 { 604 case 'bz': 605 $res = ($remaining > 0) ? bzread($this->fh, $remaining) : bzread($this->fh, $this->chunksize); 606 607 break; 608 609 case 'gz': 610 $res = ($remaining > 0) ? gzread($this->fh, $remaining) : gzread($this->fh, $this->chunksize); 611 612 break; 613 614 case 'f': 615 default: 616 $res = ($remaining > 0) ? fread($this->fh, $remaining) : fread($this->fh, $this->chunksize); 617 618 break; 619 } 620 621 if (!$res) 622 { 623 // Restore error tracking to what it was before 624 ini_set('track_errors', $trackErrors); 625 626 throw new FilesystemException($php_errormsg); 627 } 628 629 if (!$retval) 630 { 631 $retval = ''; 632 } 633 634 $retval .= $res; 635 636 if (!$this->eof()) 637 { 638 $len = \strlen($res); 639 $remaining -= $len; 640 } 641 else 642 { 643 // If it's the end of the file then we've nothing left to read; reset remaining and len 644 $remaining = 0; 645 $length = \strlen($retval); 646 } 647 } 648 while ($remaining || !$length); 649 650 // Restore error tracking to what it was before 651 ini_set('track_errors', $trackErrors); 652 653 // Return the result 654 return $retval; 655 } 656 657 /** 658 * Seek the file 659 * 660 * Note: the return value is different to that of fseek 661 * 662 * @param integer $offset Offset to use when seeking. 663 * @param integer $whence Seek mode to use. 664 * 665 * @return boolean True on success, false on failure 666 * 667 * @link https://www.php.net/manual/en/function.fseek.php 668 * @since 1.0 669 * @throws FilesystemException 670 */ 671 public function seek($offset, $whence = \SEEK_SET) 672 { 673 if (!$this->fh) 674 { 675 throw new FilesystemException('File not open'); 676 } 677 678 // Capture PHP errors 679 $php_errormsg = ''; 680 $trackErrors = ini_get('track_errors'); 681 ini_set('track_errors', true); 682 683 switch ($this->processingmethod) 684 { 685 case 'gz': 686 $res = gzseek($this->fh, $offset, $whence); 687 688 break; 689 690 case 'bz': 691 case 'f': 692 default: 693 $res = fseek($this->fh, $offset, $whence); 694 695 break; 696 } 697 698 // Restore error tracking to what it was before 699 ini_set('track_errors', $trackErrors); 700 701 // Seek, interestingly, returns 0 on success or -1 on failure. 702 if ($res == -1) 703 { 704 throw new FilesystemException($php_errormsg); 705 } 706 707 // Return the result 708 return true; 709 } 710 711 /** 712 * Returns the current position of the file read/write pointer. 713 * 714 * @return integer 715 * 716 * @since 1.0 717 * @throws FilesystemException 718 */ 719 public function tell() 720 { 721 if (!$this->fh) 722 { 723 throw new FilesystemException('File not open'); 724 } 725 726 // Capture PHP errors 727 $php_errormsg = ''; 728 $trackErrors = ini_get('track_errors'); 729 ini_set('track_errors', true); 730 731 switch ($this->processingmethod) 732 { 733 case 'gz': 734 $res = gztell($this->fh); 735 736 break; 737 738 case 'bz': 739 case 'f': 740 default: 741 $res = ftell($this->fh); 742 743 break; 744 } 745 746 // Restore error tracking to what it was before 747 ini_set('track_errors', $trackErrors); 748 749 // May return 0 so check if it's really false 750 if ($res === false) 751 { 752 throw new FilesystemException($php_errormsg); 753 } 754 755 // Return the result 756 return $res; 757 } 758 759 /** 760 * File write 761 * 762 * Whilst this function accepts a reference, the underlying fwrite 763 * will do a copy! This will roughly double the memory allocation for 764 * any write you do. Specifying chunked will get around this by only 765 * writing in specific chunk sizes. This defaults to 8192 which is a 766 * sane number to use most of the time (change the default with 767 * Stream::set('chunksize', newsize);) 768 * Note: This doesn't support gzip/bzip2 writing like reading does 769 * 770 * @param string $string Reference to the string to write. 771 * @param integer $length Length of the string to write. 772 * @param integer $chunk Size of chunks to write in. 773 * 774 * @return boolean 775 * 776 * @link https://www.php.net/manual/en/function.fwrite.php 777 * @since 1.0 778 * @throws FilesystemException 779 */ 780 public function write(&$string, $length = 0, $chunk = 0) 781 { 782 if (!$this->fh) 783 { 784 throw new FilesystemException('File not open'); 785 } 786 787 if ($this->openmode == 'r') 788 { 789 throw new \RuntimeException('File is in readonly mode'); 790 } 791 792 // If the length isn't set, set it to the length of the string. 793 if (!$length) 794 { 795 $length = \strlen($string); 796 } 797 798 // If the chunk isn't set, set it to the default. 799 if (!$chunk) 800 { 801 $chunk = $this->chunksize; 802 } 803 804 $retval = true; 805 806 // Capture PHP errors 807 $php_errormsg = ''; 808 $trackErrors = ini_get('track_errors'); 809 ini_set('track_errors', true); 810 $remaining = $length; 811 $start = 0; 812 813 do 814 { 815 // If the amount remaining is greater than the chunk size, then use the chunk 816 $amount = ($remaining > $chunk) ? $chunk : $remaining; 817 $res = fwrite($this->fh, substr($string, $start), $amount); 818 819 // Returns false on error or the number of bytes written 820 if ($res === false) 821 { 822 // Restore error tracking to what it was before 823 ini_set('track_errors', $trackErrors); 824 825 // Returned error 826 throw new FilesystemException($php_errormsg); 827 } 828 829 if ($res === 0) 830 { 831 // Restore error tracking to what it was before 832 ini_set('track_errors', $trackErrors); 833 834 // Wrote nothing? 835 throw new FilesystemException('Warning: No data written'); 836 } 837 838 // Wrote something 839 $start += $amount; 840 $remaining -= $res; 841 } 842 while ($remaining); 843 844 // Restore error tracking to what it was before. 845 ini_set('track_errors', $trackErrors); 846 847 // Return the result 848 return $retval; 849 } 850 851 /** 852 * Chmod wrapper 853 * 854 * @param string $filename File name. 855 * @param mixed $mode Mode to use. 856 * 857 * @return boolean 858 * 859 * @since 1.0 860 * @throws FilesystemException 861 */ 862 public function chmod($filename = '', $mode = 0) 863 { 864 if (!$filename) 865 { 866 if (!isset($this->filename) || !$this->filename) 867 { 868 throw new FilesystemException('Filename not set'); 869 } 870 871 $filename = $this->filename; 872 } 873 874 // If no mode is set use the default 875 if (!$mode) 876 { 877 $mode = $this->filemode; 878 } 879 880 // Capture PHP errors 881 $php_errormsg = ''; 882 $trackErrors = ini_get('track_errors'); 883 ini_set('track_errors', true); 884 $sch = parse_url($filename, \PHP_URL_SCHEME); 885 886 // Scheme specific options; ftp's chmod support is fun. 887 switch ($sch) 888 { 889 case 'ftp': 890 case 'ftps': 891 $res = Helper::ftpChmod($filename, $mode); 892 893 break; 894 895 default: 896 $res = chmod($filename, $mode); 897 898 break; 899 } 900 901 // Restore error tracking to what it was before. 902 ini_set('track_errors', $trackErrors); 903 904 // Seek, interestingly, returns 0 on success or -1 on failure 905 if ($res === false) 906 { 907 throw new FilesystemException($php_errormsg); 908 } 909 910 // Return the result 911 return true; 912 } 913 914 /** 915 * Get the stream metadata 916 * 917 * @return array header/metadata 918 * 919 * @link https://www.php.net/manual/en/function.stream-get-meta-data.php 920 * @since 1.0 921 * @throws FilesystemException 922 */ 923 public function get_meta_data() 924 { 925 if (!$this->fh) 926 { 927 throw new FilesystemException('File not open'); 928 } 929 930 return stream_get_meta_data($this->fh); 931 } 932 933 /** 934 * Stream contexts 935 * Builds the context from the array 936 * 937 * @return void 938 * 939 * @since 1.0 940 */ 941 public function _buildContext() 942 { 943 // According to the manual this always works! 944 if (\count($this->contextOptions)) 945 { 946 $this->context = @stream_context_create($this->contextOptions); 947 } 948 else 949 { 950 $this->context = null; 951 } 952 } 953 954 /** 955 * Updates the context to the array 956 * 957 * Format is the same as the options for stream_context_create 958 * 959 * @param array $context Options to create the context with 960 * 961 * @return void 962 * 963 * @link https://www.php.net/stream_context_create 964 * @since 1.0 965 */ 966 public function setContextOptions($context) 967 { 968 $this->contextOptions = $context; 969 $this->_buildContext(); 970 } 971 972 /** 973 * Adds a particular options to the context 974 * 975 * @param string $wrapper The wrapper to use 976 * @param string $name The option to set 977 * @param string $value The value of the option 978 * 979 * @return void 980 * 981 * @link https://www.php.net/stream_context_create Stream Context Creation 982 * @link https://www.php.net/manual/en/context.php Context Options for various streams 983 * @since 1.0 984 */ 985 public function addContextEntry($wrapper, $name, $value) 986 { 987 $this->contextOptions[$wrapper][$name] = $value; 988 $this->_buildContext(); 989 } 990 991 /** 992 * Deletes a particular setting from a context 993 * 994 * @param string $wrapper The wrapper to use 995 * @param string $name The option to unset 996 * 997 * @return void 998 * 999 * @link https://www.php.net/stream_context_create 1000 * @since 1.0 1001 */ 1002 public function deleteContextEntry($wrapper, $name) 1003 { 1004 // Check whether the wrapper is set 1005 if (isset($this->contextOptions[$wrapper])) 1006 { 1007 // Check that entry is set for that wrapper 1008 if (isset($this->contextOptions[$wrapper][$name])) 1009 { 1010 // Unset the item 1011 unset($this->contextOptions[$wrapper][$name]); 1012 1013 // Check that there are still items there 1014 if (!\count($this->contextOptions[$wrapper])) 1015 { 1016 // Clean up an empty wrapper context option 1017 unset($this->contextOptions[$wrapper]); 1018 } 1019 } 1020 } 1021 1022 // Rebuild the context and apply it to the stream 1023 $this->_buildContext(); 1024 } 1025 1026 /** 1027 * Applies the current context to the stream 1028 * 1029 * Use this to change the values of the context after you've opened a stream 1030 * 1031 * @return boolean 1032 * 1033 * @since 1.0 1034 * @throws FilesystemException 1035 */ 1036 public function applyContextToStream() 1037 { 1038 $retval = false; 1039 1040 if ($this->fh) 1041 { 1042 // Capture PHP errors 1043 $php_errormsg = 'Unknown error setting context option'; 1044 $trackErrors = ini_get('track_errors'); 1045 ini_set('track_errors', true); 1046 $retval = @stream_context_set_option($this->fh, $this->contextOptions); 1047 1048 // Restore error tracking to what it was before 1049 ini_set('track_errors', $trackErrors); 1050 1051 if (!$retval) 1052 { 1053 throw new FilesystemException($php_errormsg); 1054 } 1055 } 1056 1057 return $retval; 1058 } 1059 1060 /** 1061 * Stream filters 1062 * Append a filter to the chain 1063 * 1064 * @param string $filtername The key name of the filter. 1065 * @param integer $readWrite Optional. Defaults to STREAM_FILTER_READ. 1066 * @param array $params An array of params for the stream_filter_append call. 1067 * 1068 * @return resource|boolean 1069 * 1070 * @link https://www.php.net/manual/en/function.stream-filter-append.php 1071 * @since 1.0 1072 * @throws FilesystemException 1073 */ 1074 public function appendFilter($filtername, $readWrite = \STREAM_FILTER_READ, $params = array()) 1075 { 1076 $res = false; 1077 1078 if ($this->fh) 1079 { 1080 // Capture PHP errors 1081 $php_errormsg = ''; 1082 $trackErrors = ini_get('track_errors'); 1083 ini_set('track_errors', true); 1084 1085 $res = @stream_filter_append($this->fh, $filtername, $readWrite, $params); 1086 1087 // Restore error tracking to what it was before. 1088 ini_set('track_errors', $trackErrors); 1089 1090 if (!$res && $php_errormsg) 1091 { 1092 throw new FilesystemException($php_errormsg); 1093 } 1094 1095 $this->filters[] = &$res; 1096 } 1097 1098 return $res; 1099 } 1100 1101 /** 1102 * Prepend a filter to the chain 1103 * 1104 * @param string $filtername The key name of the filter. 1105 * @param integer $readWrite Optional. Defaults to STREAM_FILTER_READ. 1106 * @param array $params An array of params for the stream_filter_prepend call. 1107 * 1108 * @return resource|boolean 1109 * 1110 * @link https://www.php.net/manual/en/function.stream-filter-prepend.php 1111 * @since 1.0 1112 * @throws FilesystemException 1113 */ 1114 public function prependFilter($filtername, $readWrite = \STREAM_FILTER_READ, $params = array()) 1115 { 1116 $res = false; 1117 1118 if ($this->fh) 1119 { 1120 // Capture PHP errors 1121 $php_errormsg = ''; 1122 $trackErrors = ini_get('track_errors'); 1123 ini_set('track_errors', true); 1124 $res = @stream_filter_prepend($this->fh, $filtername, $readWrite, $params); 1125 1126 // Restore error tracking to what it was before. 1127 ini_set('track_errors', $trackErrors); 1128 1129 if (!$res && $php_errormsg) 1130 { 1131 // Set the error msg 1132 throw new FilesystemException($php_errormsg); 1133 } 1134 1135 array_unshift($this->filters, ''); 1136 $this->filters[0] = &$res; 1137 } 1138 1139 return $res; 1140 } 1141 1142 /** 1143 * Remove a filter, either by resource (handed out from the append or prepend function) 1144 * or via getting the filter list) 1145 * 1146 * @param resource $resource The resource. 1147 * @param boolean $byindex The index of the filter. 1148 * 1149 * @return boolean Result of operation 1150 * 1151 * @since 1.0 1152 * @throws FilesystemException 1153 */ 1154 public function removeFilter(&$resource, $byindex = false) 1155 { 1156 // Capture PHP errors 1157 $php_errormsg = ''; 1158 $trackErrors = ini_get('track_errors'); 1159 ini_set('track_errors', true); 1160 1161 if ($byindex) 1162 { 1163 $res = stream_filter_remove($this->filters[$resource]); 1164 } 1165 else 1166 { 1167 $res = stream_filter_remove($resource); 1168 } 1169 1170 // Restore error tracking to what it was before. 1171 ini_set('track_errors', $trackErrors); 1172 1173 if (!$res) 1174 { 1175 throw new FilesystemException($php_errormsg); 1176 } 1177 1178 return $res; 1179 } 1180 1181 /** 1182 * Copy a file from src to dest 1183 * 1184 * @param string $src The file path to copy from. 1185 * @param string $dest The file path to copy to. 1186 * @param resource $context A valid context resource (optional) created with stream_context_create. 1187 * @param boolean $usePrefix Controls the use of a prefix (optional). 1188 * @param boolean $relative Determines if the filename given is relative. Relative paths do not have JPATH_ROOT stripped. 1189 * 1190 * @return boolean 1191 * 1192 * @since 1.0 1193 * @throws FilesystemException 1194 */ 1195 public function copy($src, $dest, $context = null, $usePrefix = true, $relative = false) 1196 { 1197 // Capture PHP errors 1198 $trackErrors = ini_get('track_errors'); 1199 ini_set('track_errors', true); 1200 1201 $chmodDest = $this->_getFilename($dest, 'w', $usePrefix, $relative); 1202 1203 // Since we're going to open the file directly we need to get the filename. 1204 // We need to use the same prefix so force everything to write. 1205 $src = $this->_getFilename($src, 'w', $usePrefix, $relative); 1206 $dest = $this->_getFilename($dest, 'w', $usePrefix, $relative); 1207 1208 // One supplied at copy; overrides everything 1209 if ($context) 1210 { 1211 // Use the provided context 1212 $res = @copy($src, $dest, $context); 1213 } 1214 elseif ($this->context) 1215 { 1216 // One provided at initialisation 1217 $res = @copy($src, $dest, $this->context); 1218 } 1219 else 1220 { 1221 // No context; all defaults 1222 $res = @copy($src, $dest); 1223 } 1224 1225 // Restore error tracking to what it was before 1226 ini_set('track_errors', $trackErrors); 1227 1228 if (!$res && $php_errormsg) 1229 { 1230 throw new FilesystemException($php_errormsg); 1231 } 1232 1233 $this->chmod($chmodDest); 1234 1235 return $res; 1236 } 1237 1238 /** 1239 * Moves a file 1240 * 1241 * @param string $src The file path to move from. 1242 * @param string $dest The file path to move to. 1243 * @param resource $context A valid context resource (optional) created with stream_context_create. 1244 * @param boolean $usePrefix Controls the use of a prefix (optional). 1245 * @param boolean $relative Determines if the filename given is relative. Relative paths do not have JPATH_ROOT stripped. 1246 * 1247 * @return boolean 1248 * 1249 * @since 1.0 1250 * @throws FilesystemException 1251 */ 1252 public function move($src, $dest, $context = null, $usePrefix = true, $relative = false) 1253 { 1254 // Capture PHP errors 1255 $php_errormsg = ''; 1256 $trackErrors = ini_get('track_errors'); 1257 ini_set('track_errors', true); 1258 1259 $src = $this->_getFilename($src, 'w', $usePrefix, $relative); 1260 $dest = $this->_getFilename($dest, 'w', $usePrefix, $relative); 1261 1262 if ($context) 1263 { 1264 // Use the provided context 1265 $res = @rename($src, $dest, $context); 1266 } 1267 elseif ($this->context) 1268 { 1269 // Use the object's context 1270 $res = @rename($src, $dest, $this->context); 1271 } 1272 else 1273 { 1274 // Don't use any context 1275 $res = @rename($src, $dest); 1276 } 1277 1278 // Restore error tracking to what it was before 1279 ini_set('track_errors', $trackErrors); 1280 1281 if (!$res) 1282 { 1283 throw new FilesystemException($php_errormsg); 1284 } 1285 1286 $this->chmod($dest); 1287 1288 return $res; 1289 } 1290 1291 /** 1292 * Delete a file 1293 * 1294 * @param string $filename The file path to delete. 1295 * @param resource $context A valid context resource (optional) created with stream_context_create. 1296 * @param boolean $usePrefix Controls the use of a prefix (optional). 1297 * @param boolean $relative Determines if the filename given is relative. Relative paths do not have JPATH_ROOT stripped. 1298 * 1299 * @return boolean 1300 * 1301 * @since 1.0 1302 * @throws FilesystemException 1303 */ 1304 public function delete($filename, $context = null, $usePrefix = true, $relative = false) 1305 { 1306 // Capture PHP errors 1307 $php_errormsg = ''; 1308 $trackErrors = ini_get('track_errors'); 1309 ini_set('track_errors', true); 1310 1311 $filename = $this->_getFilename($filename, 'w', $usePrefix, $relative); 1312 1313 if ($context) 1314 { 1315 // Use the provided context 1316 $res = @unlink($filename, $context); 1317 } 1318 elseif ($this->context) 1319 { 1320 // Use the object's context 1321 $res = @unlink($filename, $this->context); 1322 } 1323 else 1324 { 1325 // Don't use any context 1326 $res = @unlink($filename); 1327 } 1328 1329 // Restore error tracking to what it was before. 1330 ini_set('track_errors', $trackErrors); 1331 1332 if (!$res) 1333 { 1334 throw new FilesystemException($php_errormsg); 1335 } 1336 1337 return $res; 1338 } 1339 1340 /** 1341 * Upload a file 1342 * 1343 * @param string $src The file path to copy from (usually a temp folder). 1344 * @param string $dest The file path to copy to. 1345 * @param resource $context A valid context resource (optional) created with stream_context_create. 1346 * @param boolean $usePrefix Controls the use of a prefix (optional). 1347 * @param boolean $relative Determines if the filename given is relative. Relative paths do not have JPATH_ROOT stripped. 1348 * 1349 * @return boolean 1350 * 1351 * @since 1.0 1352 * @throws FilesystemException 1353 */ 1354 public function upload($src, $dest, $context = null, $usePrefix = true, $relative = false) 1355 { 1356 if (is_uploaded_file($src)) 1357 { 1358 // Make sure it's an uploaded file 1359 return $this->copy($src, $dest, $context, $usePrefix, $relative); 1360 } 1361 1362 throw new FilesystemException('Not an uploaded file.'); 1363 } 1364 1365 /** 1366 * Writes a chunk of data to a file. 1367 * 1368 * @param string $filename The file name. 1369 * @param string $buffer The data to write to the file. 1370 * @param boolean $appendToFile Append to the file and not overwrite it. 1371 * 1372 * @return boolean 1373 * 1374 * @since 1.0 1375 */ 1376 public function writeFile($filename, &$buffer, $appendToFile = false) 1377 { 1378 $fileMode = 'w'; 1379 1380 // Switch the filemode when we want to append to the file 1381 if ($appendToFile) 1382 { 1383 $fileMode = 'a'; 1384 } 1385 1386 if ($this->open($filename, $fileMode)) 1387 { 1388 $result = $this->write($buffer); 1389 $this->chmod(); 1390 $this->close(); 1391 1392 return $result; 1393 } 1394 1395 return false; 1396 } 1397 1398 /** 1399 * Determine the appropriate 'filename' of a file 1400 * 1401 * @param string $filename Original filename of the file 1402 * @param string $mode Mode string to retrieve the filename 1403 * @param boolean $usePrefix Controls the use of a prefix 1404 * @param boolean $relative Determines if the filename given is relative. Relative paths do not have JPATH_ROOT stripped. 1405 * 1406 * @return string 1407 * 1408 * @since 1.0 1409 */ 1410 public function _getFilename($filename, $mode, $usePrefix, $relative) 1411 { 1412 if ($usePrefix) 1413 { 1414 // Get rid of binary or t, should be at the end of the string 1415 $tmode = trim($mode, 'btf123456789'); 1416 1417 $stream = explode('://', $filename, 2); 1418 $scheme = ''; 1419 $filename = $stream[0]; 1420 1421 if (\count($stream) >= 2) 1422 { 1423 $scheme = $stream[0] . '://'; 1424 $filename = $stream[1]; 1425 } 1426 1427 // Check if it's a write mode then add the appropriate prefix 1428 if (\in_array($tmode, Helper::getWriteModes())) 1429 { 1430 $prefixToUse = $this->writeprefix; 1431 } 1432 else 1433 { 1434 $prefixToUse = $this->readprefix; 1435 } 1436 1437 // Get rid of JPATH_ROOT (legacy compat) 1438 if (!$relative && $prefixToUse) 1439 { 1440 $pos = strpos($filename, JPATH_ROOT); 1441 1442 if ($pos !== false) 1443 { 1444 $filename = substr_replace($filename, '', $pos, \strlen(JPATH_ROOT)); 1445 } 1446 } 1447 1448 $filename = ($prefixToUse ? $prefixToUse : '') . $filename; 1449 } 1450 1451 return $filename; 1452 } 1453 1454 /** 1455 * Return the internal file handle 1456 * 1457 * @return File handler 1458 * 1459 * @since 1.0 1460 */ 1461 public function getFileHandle() 1462 { 1463 return $this->fh; 1464 } 1465 1466 /** 1467 * Modifies a property of the object, creating it if it does not already exist. 1468 * 1469 * @param string $property The name of the property. 1470 * @param mixed $value The value of the property to set. 1471 * 1472 * @return mixed Previous value of the property. 1473 * 1474 * @since 1.0 1475 */ 1476 public function set($property, $value = null) 1477 { 1478 $previous = isset($this->$property) ? $this->$property : null; 1479 $this->$property = $value; 1480 1481 return $previous; 1482 } 1483 1484 /** 1485 * Returns a property of the object or the default value if the property is not set. 1486 * 1487 * @param string $property The name of the property. 1488 * @param mixed $default The default value. 1489 * 1490 * @return mixed The value of the property. 1491 * 1492 * @since 1.0 1493 */ 1494 public function get($property, $default = null) 1495 { 1496 if (isset($this->$property)) 1497 { 1498 return $this->$property; 1499 } 1500 1501 return $default; 1502 } 1503} 1504