From 26d71bd792400a839655007ad54724ec9181e6ed Mon Sep 17 00:00:00 2001 From: PJ Dietz Date: Thu, 23 Apr 2015 21:53:54 -0400 Subject: [PATCH] Add UploadedFile --- src/Message/UploadedFile.php | 167 +++++++++++++++++++ test/tests/unit/Message/UploadedFileTest.php | 154 +++++++++++++++++ 2 files changed, 321 insertions(+) create mode 100644 src/Message/UploadedFile.php create mode 100644 test/tests/unit/Message/UploadedFileTest.php diff --git a/src/Message/UploadedFile.php b/src/Message/UploadedFile.php new file mode 100644 index 0000000..0678aa2 --- /dev/null +++ b/src/Message/UploadedFile.php @@ -0,0 +1,167 @@ +clientFilename = $name; + $this->error = $error; + $this->clientMediaType = $type; + $this->size = $size; + + if (file_exists($tmpName)) { + $this->tmpName = $tmpName; + $this->stream = new Stream(fopen($tmpName, "r")); + } else { + $this->stream = new NullStream(); + } + } + + /** + * Retrieve a stream representing the uploaded file. + * + * This method MUST return a StreamInterface instance, representing the + * uploaded file. The purpose of this method is to allow utilizing native PHP + * stream functionality to manipulate the file upload, such as + * stream_copy_to_stream() (though the result will need to be decorated in a + * native PHP stream wrapper to work with such functions). + * + * If the move() method has been called previously, this method MUST raise + * an exception. + * + * @return StreamInterface Stream representation of the uploaded file. + * @throws \RuntimeException in cases when no stream is available or can be + * created. + */ + public function getStream() + { + if ($this->moved) { + throw new \RuntimeException("File has already been moved"); + } + return $this->stream; + } + + /** + * Move the uploaded file to a new location. + * + * Use this method as an alternative to move_uploaded_file(). This method is + * guaranteed to work in both SAPI and non-SAPI environments. + * Implementations must determine which environment they are in, and use the + * appropriate method (move_uploaded_file(), rename(), or a stream + * operation) to perform the operation. + * + * The original file or stream MUST be removed on completion. + * + * If this method is called more than once, any subsequent calls MUST raise + * an exception. + * + * When used in an SAPI environment where $_FILES is populated, when writing + * files via move(), is_uploaded_file() and move_uploaded_file() SHOULD be + * use to ensure permissions and upload status are verified correctly. + * + * @see http://php.net/is_uploaded_file + * @see http://php.net/move_uploaded_file + * @param string $path Path to which to move the uploaded file. + * @throws \InvalidArgumentException if the $path specified is invalid. + * @throws \RuntimeException on any error during the move operation, or on + * the second or subsequent call to the method. + */ + public function move($path) + { + if ($this->tmpName === null || !file_exists($this->tmpName)) { + throw new \RuntimeException("File " . $this->tmpName . " does not exist."); + } + $sapi = php_sapi_name(); + $whitelist = ["apache", "apache2filter", "apache2handler", "cgi", "fpm-fcgi", "cgi-fcgi"]; + if (in_array($sapi, $whitelist)) { + // @codeCoverageIgnoreStart + move_uploaded_file($this->tmpName, $path); + } else { + // @codeCoverageIgnoreEnd + rename($this->tmpName, $path); + } + $this->moved = true; + } + + /** + * Retrieve the file size. + * + * Implementations SHOULD return the value stored in the "size" key of + * the file in the $_FILES array if available, as PHP calculates this based + * on the actual size transmitted. + * + * @return int|null The file size in bytes or null if unknown. + */ + public function getSize() + { + return $this->size; + } + + /** + * Retrieve the error associated with the uploaded file. + * + * The return value MUST be one of PHP's UPLOAD_ERR_XXX constants. + * + * If the file was uploaded successfully, this method MUST return + * UPLOAD_ERR_OK. + * + * Implementations SHOULD return the value stored in the "error" key of + * the file in the $_FILES array. + * + * @see http://php.net/manual/en/features.file-upload.errors.php + * @return int One of PHP's UPLOAD_ERR_XXX constants. + */ + public function getError() + { + return $this->error; + } + + /** + * Retrieve the filename sent by the client. + * + * Do not trust the value returned by this method. A client could send + * a malicious filename with the intention to corrupt or hack your + * application. + * + * Implementations SHOULD return the value stored in the "name" key of + * the file in the $_FILES array. + * + * @return string|null The filename sent by the client or null if none + * was provided. + */ + public function getClientFilename() + { + return $this->clientFilename; + } + + /** + * Retrieve the media type sent by the client. + * + * Do not trust the value returned by this method. A client could send + * a malicious media type with the intention to corrupt or hack your + * application. + * + * Implementations SHOULD return the value stored in the "type" key of + * the file in the $_FILES array. + * + * @return string|null The media type sent by the client or null if none + * was provided. + */ + public function getClientMediaType() + { + return $this->clientMediaType; + } +} diff --git a/test/tests/unit/Message/UploadedFileTest.php b/test/tests/unit/Message/UploadedFileTest.php new file mode 100644 index 0000000..f13b363 --- /dev/null +++ b/test/tests/unit/Message/UploadedFileTest.php @@ -0,0 +1,154 @@ +tmpName = tempnam(sys_get_temp_dir(), "tst"); + $this->movePath = tempnam(sys_get_temp_dir(), "tst"); + } + + public function tearDown() + { + parent::tearDown(); + if (file_exists($this->tmpName)) { + unlink($this->tmpName); + } + if (file_exists($this->movePath)) { + unlink($this->movePath); + } + } + + // ------------------------------------------------------------------------ + // getStream + + /** + * @covers WellRESTed\Message\UploadedFile::__construct + * @covers WellRESTed\Message\UploadedFile::getStream + */ + public function testGetStreamReturnsStreamInterface() + { + $file = new UploadedFile("", "", 0, "", 0); + $this->assertInstanceOf('\Psr\Http\Message\StreamInterface', $file->getStream()); + } + + /** + * @covers WellRESTed\Message\UploadedFile::__construct + * @covers WellRESTed\Message\UploadedFile::getStream + */ + public function testGetStreamReturnsStreamWrappingUploadedFile() + { + $content = "Hello, World!"; + file_put_contents($this->tmpName, $content); + + $file = new UploadedFile("", "", 0, $this->tmpName, ""); + $stream = $file->getStream(); + $this->assertEquals($content, (string) $stream); + } + + /** + * @covers WellRESTed\Message\UploadedFile::__construct + * @covers WellRESTed\Message\UploadedFile::getStream + */ + public function testGetStreamReturnsEmptyStreamForNoFile() + { + $file = new UploadedFile("", "", 0, "", 0); + $this->assertTrue($file->getStream()->eof()); + } + + /** + * @covers WellRESTed\Message\UploadedFile::__construct + * @covers WellRESTed\Message\UploadedFile::getStream + * @expectedException \RuntimeException + */ + public function testGetStreamThrowsExceptionAfterMove() + { + $content = "Hello, World!"; + file_put_contents($this->tmpName, $content); + + $file = new UploadedFile("", "", 0, $this->tmpName, ""); + $file->move($this->movePath); + $file->getStream(); + } + + // ------------------------------------------------------------------------ + // move + + /** + * @covers WellRESTed\Message\UploadedFile::move + */ + public function testMoveRelocatesUploadedFileToDestiationIfExists() + { + $content = "Hello, World!"; + file_put_contents($this->tmpName, $content); + $originalMd5 = md5_file($this->tmpName); + + $file = new UploadedFile("", "", 0, $this->tmpName, ""); + $file->move($this->movePath); + + $this->assertEquals($originalMd5, md5_file($this->movePath)); + } + + /** + * @covers WellRESTed\Message\UploadedFile::move + * @expectedException \RuntimeException + */ + public function testThrowsExcpetionOnSubsequentCallToMove() + { + $content = "Hello, World!"; + file_put_contents($this->tmpName, $content); + + $file = new UploadedFile("", "", 0, $this->tmpName, ""); + $file->move($this->movePath); + $file->move($this->movePath); + } + + // ------------------------------------------------------------------------ + // getSize + + public function testGetSizeReturnsSize() + { + $file = new UploadedFile("", "", 1024, "", 0); + $this->assertEquals(1024, $file->getSize()); + } + + // ------------------------------------------------------------------------ + // getError + + public function testGetErrorReturnsError() + { + $file = new UploadedFile("", "", 1024, "", UPLOAD_ERR_INI_SIZE); + $this->assertEquals(UPLOAD_ERR_INI_SIZE, $file->getError()); + } + + // ------------------------------------------------------------------------ + // clientFilename + + public function testGetClientFilenameReturnsClientFilename() + { + $file = new UploadedFile("clientFilename", "", 0, "", 0); + $this->assertEquals("clientFilename", $file->getClientFilename()); + } + + // ------------------------------------------------------------------------ + // clientMediaType + + public function testGetClientMediaTypeReturnsClientMediaType() + { + $file = new UploadedFile("", "clientMediaType", 0, "", 0); + $this->assertEquals("clientMediaType", $file->getClientMediaType()); + } +}