diff --git a/src/Spout/Common/Helper/GlobalFunctionsHelper.php b/src/Spout/Common/Helper/GlobalFunctionsHelper.php index 8243bcc..3dcddaf 100644 --- a/src/Spout/Common/Helper/GlobalFunctionsHelper.php +++ b/src/Spout/Common/Helper/GlobalFunctionsHelper.php @@ -280,6 +280,17 @@ class GlobalFunctionsHelper return mb_convert_encoding($string, $targetEncoding, $sourceEncoding); } + /** + * Wrapper around global function stream_get_wrappers() + * @see stream_get_wrappers() + * + * @return array + */ + public function stream_get_wrappers() + { + return stream_get_wrappers(); + } + /** * Wrapper around global function stream_get_line() * @see stream_get_line() diff --git a/src/Spout/Reader/AbstractReader.php b/src/Spout/Reader/AbstractReader.php index ee6af08..d6d38e2 100644 --- a/src/Spout/Reader/AbstractReader.php +++ b/src/Spout/Reader/AbstractReader.php @@ -19,6 +19,13 @@ abstract class AbstractReader implements ReaderInterface /** @var \Box\Spout\Common\Helper\GlobalFunctionsHelper Helper to work with global functions */ protected $globalFunctionsHelper; + /** + * Returns whether stream wrappers are supported + * + * @return bool + */ + abstract protected function doesSupportStreamWrapper(); + /** * Opens the file at the given file path to make it ready to be read * @@ -62,6 +69,10 @@ abstract class AbstractReader implements ReaderInterface */ public function open($filePath) { + if ($this->isStreamWrapper($filePath) && (!$this->doesSupportStreamWrapper() || !$this->isSupportedStreamWrapper($filePath))) { + throw new IOException("Could not open $filePath for reading! Stream wrapper used is not supported for this type of file."); + } + if (!$this->isPhpStream($filePath)) { // we skip the checks if the provided file path points to a PHP stream if (!$this->globalFunctionsHelper->file_exists($filePath)) { @@ -72,8 +83,7 @@ abstract class AbstractReader implements ReaderInterface } try { - // Need to use realpath to fix "Can't open file" on some Windows setup - $fileRealPath = realpath($filePath); + $fileRealPath = $this->getFileRealPath($filePath); $this->openReader($fileRealPath); $this->isStreamOpened = true; } catch (\Exception $exception) { @@ -81,6 +91,67 @@ abstract class AbstractReader implements ReaderInterface } } + /** + * Returns the real path of the given path. + * If the given path is a valid stream wrapper, returns the path unchanged. + * + * @param string $filePath + * @return string + */ + protected function getFileRealPath($filePath) + { + if ($this->isSupportedStreamWrapper($filePath)) { + return $filePath; + } + + // Need to use realpath to fix "Can't open file" on some Windows setup + return realpath($filePath); + } + + /** + * Returns the scheme of the custom stream wrapper, if the path indicates a stream wrapper is used. + * For example, php://temp => php, s3://path/to/file => s3... + * + * @param string $filePath Path of the file to be read + * @return string|null The stream wrapper scheme or NULL if not a stream wrapper + */ + protected function getStreamWrapperScheme($filePath) + { + $streamScheme = null; + if (preg_match('/^(\w+):\/\//', $filePath, $matches)) { + $streamScheme = $matches[1]; + } + return $streamScheme; + } + + /** + * Checks if the given path is an unsupported stream wrapper + * (like local path, php://temp, mystream://foo/bar...). + * + * @param string $filePath Path of the file to be read + * @return bool Whether the given path is an unsupported stream wrapper + */ + protected function isStreamWrapper($filePath) + { + return ($this->getStreamWrapperScheme($filePath) !== null); + } + + /** + * Checks if the given path is an supported stream wrapper + * (like php://temp, mystream://foo/bar...). + * If the given path is a local path, returns true. + * + * @param string $filePath Path of the file to be read + * @return bool Whether the given path is an supported stream wrapper + */ + protected function isSupportedStreamWrapper($filePath) + { + $streamScheme = $this->getStreamWrapperScheme($filePath); + return ($streamScheme !== null) ? + in_array($streamScheme, $this->globalFunctionsHelper->stream_get_wrappers()) : + true; + } + /** * Checks if a path is a PHP stream (like php://output, php://memory, ...) * @@ -89,7 +160,8 @@ abstract class AbstractReader implements ReaderInterface */ protected function isPhpStream($filePath) { - return (strpos($filePath, 'php://') === 0); + $streamScheme = $this->getStreamWrapperScheme($filePath); + return ($streamScheme === 'php'); } /** diff --git a/src/Spout/Reader/CSV/Reader.php b/src/Spout/Reader/CSV/Reader.php index af02def..056d0a7 100644 --- a/src/Spout/Reader/CSV/Reader.php +++ b/src/Spout/Reader/CSV/Reader.php @@ -30,7 +30,7 @@ class Reader extends AbstractReader protected $encoding = EncodingHelper::ENCODING_UTF8; /** @var string Defines the End of line */ - protected $endOfLineCharacter = "\n"; + protected $endOfLineCharacter = "\n"; /** * Sets the field delimiter for the CSV. @@ -82,7 +82,17 @@ class Reader extends AbstractReader { $this->endOfLineCharacter = $endOfLineCharacter; return $this; - } + } + + /** + * Returns whether stream wrappers are supported + * + * @return bool + */ + protected function doesSupportStreamWrapper() + { + return true; + } /** * Opens the file at the given path to make it ready to be read. diff --git a/src/Spout/Reader/ODS/Reader.php b/src/Spout/Reader/ODS/Reader.php index c59ab57..b4093ae 100644 --- a/src/Spout/Reader/ODS/Reader.php +++ b/src/Spout/Reader/ODS/Reader.php @@ -19,6 +19,16 @@ class Reader extends AbstractReader /** @var SheetIterator To iterator over the ODS sheets */ protected $sheetIterator; + /** + * Returns whether stream wrappers are supported + * + * @return bool + */ + protected function doesSupportStreamWrapper() + { + return false; + } + /** * Opens the file at the given file path to make it ready to be read. * diff --git a/src/Spout/Reader/XLSX/Reader.php b/src/Spout/Reader/XLSX/Reader.php index cf13517..42c6f02 100644 --- a/src/Spout/Reader/XLSX/Reader.php +++ b/src/Spout/Reader/XLSX/Reader.php @@ -37,6 +37,16 @@ class Reader extends AbstractReader return $this; } + /** + * Returns whether stream wrappers are supported + * + * @return bool + */ + protected function doesSupportStreamWrapper() + { + return false; + } + /** * Opens the file at the given file path to make it ready to be read. * It also parses the sharedStrings.xml file to get all the shared strings available in memory diff --git a/tests/Spout/Reader/CSV/ReaderTest.php b/tests/Spout/Reader/CSV/ReaderTest.php index c4c00ba..cdbca56 100644 --- a/tests/Spout/Reader/CSV/ReaderTest.php +++ b/tests/Spout/Reader/CSV/ReaderTest.php @@ -423,4 +423,50 @@ class ReaderTest extends \PHPUnit_Framework_TestCase $this->assertEquals($expectedRows, $allRows); } + /** + * @return void + */ + public function testReadCustomStreamWrapper() + { + $allRows = []; + $resourcePath = 'spout://csv_standard'; + + // register stream wrapper + stream_wrapper_register('spout', SpoutTestStream::CLASS_NAME); + + /** @var \Box\Spout\Reader\CSV\Reader $reader */ + $reader = ReaderFactory::create(Type::CSV); + $reader->open($resourcePath); + + foreach ($reader->getSheetIterator() as $sheet) { + foreach ($sheet->getRowIterator() as $row) { + $allRows[] = $row; + } + } + + $reader->close(); + + $expectedRows = [ + ['csv--11', 'csv--12', 'csv--13'], + ['csv--21', 'csv--22', 'csv--23'], + ['csv--31', 'csv--32', 'csv--33'], + ]; + $this->assertEquals($expectedRows, $allRows); + + // cleanup + stream_wrapper_unregister('spout'); + } + + /** + * @expectedException \Box\Spout\Common\Exception\IOException + * + * @return void + */ + public function testReadWithUnsupportedCustomStreamWrapper() + { + /** @var \Box\Spout\Reader\CSV\Reader $reader */ + $reader = ReaderFactory::create(Type::CSV); + $reader->open('unsupported://foobar'); + } + } diff --git a/tests/Spout/Reader/CSV/SpoutTestStream.php b/tests/Spout/Reader/CSV/SpoutTestStream.php new file mode 100644 index 0000000..fdaeb18 --- /dev/null +++ b/tests/Spout/Reader/CSV/SpoutTestStream.php @@ -0,0 +1,120 @@ +getFilePathFromStreamPath($path); + return stat($filePath); + } + + /** + * @param string $streamPath + * @return string + */ + private function getFilePathFromStreamPath($streamPath) + { + $fileName = parse_url($streamPath, PHP_URL_HOST); + return self::PATH_TO_CSV_RESOURCES . $fileName . self::CSV_EXTENSION; + } + + /** + * @param string $path + * @param string $mode + * @param int $options + * @param string $opened_path + * @return bool + */ + public function stream_open($path, $mode, $options, &$opened_path) + { + $this->position = 0; + + // the path is like "spout://csv_name" so the actual file name correspond the name of the host. + $filePath = $this->getFilePathFromStreamPath($path); + $this->fileHandle = fopen($filePath, $mode); + + return true; + } + + /** + * @param int $numBytes + * @return string + */ + public function stream_read($numBytes) + { + $this->position += $numBytes; + return fread($this->fileHandle, $numBytes); + } + + /** + * @return int + */ + public function stream_tell() + { + return $this->position; + } + + /** + * @param int $offset + * @param int|void $whence + * @return bool + */ + public function stream_seek($offset, $whence = SEEK_SET) + { + $result = fseek($this->fileHandle, $offset, $whence); + if ($result === -1) { + return false; + } + + if ($whence === SEEK_SET) { + $this->position = $offset; + } else if ($whence === SEEK_CUR) { + $this->position += $offset; + } else { + // not implemented + } + + return true; + } + + /** + * @return bool + */ + public function stream_close() + { + return fclose($this->fileHandle); + } + + /** + * @return bool + */ + public function stream_eof() + { + return feof($this->fileHandle); + } +} diff --git a/tests/Spout/Reader/ODS/ReaderTest.php b/tests/Spout/Reader/ODS/ReaderTest.php index df04ffc..8d9977b 100644 --- a/tests/Spout/Reader/ODS/ReaderTest.php +++ b/tests/Spout/Reader/ODS/ReaderTest.php @@ -363,6 +363,30 @@ class ReaderTest extends \PHPUnit_Framework_TestCase $this->assertEquals($expectedRows, $allRows); } + /** + * @expectedException \Box\Spout\Common\Exception\IOException + * + * @return void + */ + public function testReadWithUnsupportedCustomStreamWrapper() + { + /** @var \Box\Spout\Reader\ODS\Reader $reader */ + $reader = ReaderFactory::create(Type::ODS); + $reader->open('unsupported://foobar'); + } + + /** + * @expectedException \Box\Spout\Common\Exception\IOException + * + * @return void + */ + public function testReadWithSupportedCustomStreamWrapper() + { + /** @var \Box\Spout\Reader\ODS\Reader $reader */ + $reader = ReaderFactory::create(Type::ODS); + $reader->open('php://memory'); + } + /** * @param string $fileName * @return array All the read rows the given file diff --git a/tests/Spout/Reader/XLSX/ReaderTest.php b/tests/Spout/Reader/XLSX/ReaderTest.php index 2802d84..a3bdf7b 100644 --- a/tests/Spout/Reader/XLSX/ReaderTest.php +++ b/tests/Spout/Reader/XLSX/ReaderTest.php @@ -404,6 +404,30 @@ class ReaderTest extends \PHPUnit_Framework_TestCase $this->assertEquals($expectedRows, $allRows); } + /** + * @expectedException \Box\Spout\Common\Exception\IOException + * + * @return void + */ + public function testReadWithUnsupportedCustomStreamWrapper() + { + /** @var \Box\Spout\Reader\XLSX\Reader $reader */ + $reader = ReaderFactory::create(Type::XLSX); + $reader->open('unsupported://foobar'); + } + + /** + * @expectedException \Box\Spout\Common\Exception\IOException + * + * @return void + */ + public function testReadWithSupportedCustomStreamWrapper() + { + /** @var \Box\Spout\Reader\XLSX\Reader $reader */ + $reader = ReaderFactory::create(Type::XLSX); + $reader->open('php://memory'); + } + /** * @param string $fileName * @return array All the read rows the given file diff --git a/tests/bootstrap.php b/tests/bootstrap.php index ebe7689..902b4e9 100644 --- a/tests/bootstrap.php +++ b/tests/bootstrap.php @@ -3,6 +3,7 @@ require_once(dirname(__DIR__) . '/vendor/autoload.php'); require_once(dirname(__DIR__) . '/tests/Spout/TestUsingResource.php'); require_once(dirname(__DIR__) . '/tests/Spout/ReflectionHelper.php'); +require_once(dirname(__DIR__) . '/tests/Spout/Reader/CSV/SpoutTestStream.php'); // Make sure a timezone is set to be able to work with dates date_default_timezone_set('UTC');