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