Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

merge upstream #1

Merged
merged 2 commits into from
Jan 15, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions framework/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,10 @@ Yii Framework 2 Change Log
2.0.32 under development
------------------------

- Bug #17037: Fix uploaded file saving method in case generated by `MultipartFormDataParser` (sup-ham)
- Bug #17803: Fixed ErrorHandler unregister and register only change global state when applicable (SamMousa)
- Bug #17744: Fix a bug with setting incorrect `defaultValue` to AR column with `CURRENT_TIMESTAMP(x)` as default expression (MySQL >= 5.6.4) (bizley)
- Enh #17729: Path alias support was added to `yii\web\UploadFile::saveAs()` (sup-ham)
- Bug #17749: Dispatcher fix if target crashed in PHP 7.0+ (kamarton)
- Bug #17762: PHP 7.4: Remove special condition for converting PHP errors to exceptions if they occurred inside of `__toString()` call (rob006)
- Bug #17771: migrate/fresh was not returning exit code (samdark)
Expand Down
36 changes: 24 additions & 12 deletions framework/base/ErrorHandler.php
Original file line number Diff line number Diff line change
Expand Up @@ -51,32 +51,44 @@ abstract class ErrorHandler extends Component
*/
private $_hhvmException;

/**
* @var bool whether this instance has been registered using `register()`
*/
private $_registered = false;

/**
* Register this error handler.
* @since 2.0.32 this will not do anything if the error handler was already registered
*/
public function register()
{
ini_set('display_errors', false);
set_exception_handler([$this, 'handleException']);
if (defined('HHVM_VERSION')) {
set_error_handler([$this, 'handleHhvmError']);
} else {
set_error_handler([$this, 'handleError']);
}
if ($this->memoryReserveSize > 0) {
$this->_memoryReserve = str_repeat('x', $this->memoryReserveSize);
if (!$this->_registered) {
ini_set('display_errors', false);
set_exception_handler([$this, 'handleException']);
if (defined('HHVM_VERSION')) {
set_error_handler([$this, 'handleHhvmError']);
} else {
set_error_handler([$this, 'handleError']);
}
if ($this->memoryReserveSize > 0) {
$this->_memoryReserve = str_repeat('x', $this->memoryReserveSize);
}
register_shutdown_function([$this, 'handleFatalError']);
$this->_registered = true;
}
register_shutdown_function([$this, 'handleFatalError']);
}

/**
* Unregisters this error handler by restoring the PHP error and exception handlers.
* @since 2.0.32 this will not do anything if the error handler was not registered
*/
public function unregister()
{
restore_error_handler();
restore_exception_handler();
if ($this->_registered) {
restore_error_handler();
restore_exception_handler();
$this->_registered = false;
}
}

/**
Expand Down
4 changes: 2 additions & 2 deletions framework/web/MultipartFormDataParser.php
Original file line number Diff line number Diff line change
Expand Up @@ -54,8 +54,7 @@
*
* > Note: although this parser fully emulates regular structure of the `$_FILES`, related temporary
* files, which are available via `tmp_name` key, will not be recognized by PHP as uploaded ones.
* Thus functions like `is_uploaded_file()` and `move_uploaded_file()` will fail on them. This also
* means [[UploadedFile::saveAs()]] will fail as well.
* Thus functions like `is_uploaded_file()` and `move_uploaded_file()` will fail on them.
*
* @property int $uploadFileMaxCount Maximum upload files count.
* @property int $uploadFileMaxSize Upload file max size in bytes.
Expand Down Expand Up @@ -191,6 +190,7 @@ public function parse($rawBody, $contentType)
@fclose($tmpResource);
} else {
fwrite($tmpResource, $value);
rewind($tmpResource);
$fileInfo['tmp_name'] = $tmpFileName;
$fileInfo['tmp_resource'] = $tmpResource; // save file resource, otherwise it will be deleted
}
Expand Down
90 changes: 77 additions & 13 deletions framework/web/UploadedFile.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,9 @@

namespace yii\web;

use Yii;
use yii\base\BaseObject;
use yii\helpers\ArrayHelper;
use yii\helpers\Html;

/**
Expand Down Expand Up @@ -56,9 +58,25 @@ class UploadedFile extends BaseObject
*/
public $error;

/**
* @var resource a temporary uploaded stream resource used within PUT and PATCH request.
*/
private $_tempResource;

private static $_files;


/**
* UploadedFile constructor.
*
* @param array $config name-value pairs that will be used to initialize the object properties
*/
public function __construct($config = [])
{
$this->_tempResource = ArrayHelper::remove($config, 'tempResource');
parent::__construct($config);
}

/**
* String output.
* This is PHP magic method that returns string representation of an object.
Expand Down Expand Up @@ -149,25 +167,68 @@ public static function reset()

/**
* Saves the uploaded file.
* Note that this method uses php's move_uploaded_file() method. If the target file `$file`
* already exists, it will be overwritten.
* @param string $file the file path used to save the uploaded file
* If the target file `$file` already exists, it will be overwritten.
* @param string $file the file path or a path alias used to save the uploaded file.
* @param bool $deleteTempFile whether to delete the temporary file after saving.
* If true, you will not be able to save the uploaded file again in the current request.
* @return bool true whether the file is saved successfully
* @see error
*/
public function saveAs($file, $deleteTempFile = true)
{
if ($this->error == UPLOAD_ERR_OK) {
if ($deleteTempFile) {
return move_uploaded_file($this->tempName, $file);
} elseif (is_uploaded_file($this->tempName)) {
return copy($this->tempName, $file);
}
if ($this->error !== UPLOAD_ERR_OK) {
return false;
}
if (false === $this->copyTempFile(Yii::getAlias($file))) {
return false;
}
return !$deleteTempFile || $this->deleteTempFile();
}

return false;
/**
* Copy temporary file into file specified
*
* @param string $targetFile path of the file to copy to
* @return bool|int the total count of bytes copied, or false on failure
*/
protected function copyTempFile($targetFile)
{
if (!is_resource($this->_tempResource)) {
return $this->isUploadedFile($this->tempName) && copy($this->tempName, $targetFile);
}
$target = fopen($targetFile, 'wb');
if ($target === false) {
return false;
}

$result = stream_copy_to_stream($this->_tempResource, $target);
@fclose($target);

return $result;
}

/**
* Delete temporary file
*
* @return bool if file was deleted
*/
protected function deleteTempFile()
{
if (is_resource($this->_tempResource)) {
return @fclose($this->_tempResource);
}
return $this->isUploadedFile($this->tempName) && @unlink($this->tempName);
}

/**
* Check if file is uploaded file
*
* @param string $file path to the file to check
* @return bool
*/
protected function isUploadedFile($file)
{
return is_uploaded_file($file);
}

/**
Expand Down Expand Up @@ -207,7 +268,8 @@ private static function loadFiles()
self::$_files = [];
if (isset($_FILES) && is_array($_FILES)) {
foreach ($_FILES as $class => $info) {
self::loadFilesRecursive($class, $info['name'], $info['tmp_name'], $info['type'], $info['size'], $info['error']);
$resource = isset($info['tmp_resource']) ? $info['tmp_resource'] : [];
self::loadFilesRecursive($class, $info['name'], $info['tmp_name'], $info['type'], $info['size'], $info['error'], $resource);
}
}
}
Expand All @@ -224,16 +286,18 @@ private static function loadFiles()
* @param mixed $sizes file sizes provided by PHP
* @param mixed $errors uploading issues provided by PHP
*/
private static function loadFilesRecursive($key, $names, $tempNames, $types, $sizes, $errors)
private static function loadFilesRecursive($key, $names, $tempNames, $types, $sizes, $errors, $tempResources)
{
if (is_array($names)) {
foreach ($names as $i => $name) {
self::loadFilesRecursive($key . '[' . $i . ']', $name, $tempNames[$i], $types[$i], $sizes[$i], $errors[$i]);
$resource = isset($tempResources[$i]) ? $tempResources[$i] : [];
self::loadFilesRecursive($key . '[' . $i . ']', $name, $tempNames[$i], $types[$i], $sizes[$i], $errors[$i], $resource);
}
} elseif ((int) $errors !== UPLOAD_ERR_NO_FILE) {
self::$_files[$key] = [
'name' => $names,
'tempName' => $tempNames,
'tempResource' => $tempResources,
'type' => $types,
'size' => $sizes,
'error' => $errors,
Expand Down
41 changes: 41 additions & 0 deletions tests/framework/web/UploadedFileTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
namespace yiiunit\framework\web;

use yii\web\UploadedFile;
use yiiunit\framework\web\mocks\UploadedFileMock;
use yiiunit\framework\web\stubs\ModelStub;
use yiiunit\framework\web\stubs\VendorImage;
use yiiunit\TestCase;
Expand Down Expand Up @@ -35,6 +36,17 @@ private function generateFakeFileData()
];
}

private function generateTempFileData()
{
return [
'name' => md5(mt_rand()),
'tmp_name' => tempnam(sys_get_temp_dir(), ''),
'type' => 'image/jpeg',
'size' => mt_rand(1000, 10000),
'error' => 0,
];
}

private function generateFakeFiles()
{
$_FILES['ModelStub[prod_image]'] = $this->generateFakeFileData();
Expand All @@ -46,6 +58,8 @@ private function generateFakeFiles()
$_FILES['ModelStub[vendor_images][]'] = $this->generateFakeFileData();
$_FILES['ModelStub[vendor_images][]'] = $this->generateFakeFileData();
$_FILES['ModelStub[vendor_images][]'] = $this->generateFakeFileData();

$_FILES['ModelStub[temp_image]'] = $this->generateTempFileData();
}

// Tests :
Expand All @@ -72,4 +86,31 @@ public function testGetInstances()
$this->assertInstanceOf(VendorImage::className(), $vendorImage);
}
}

public function testSaveAs()
{
$tmpImage = UploadedFileMock::getInstance(new ModelStub(), 'temp_image');
$targetFile = '@runtime/test_saved_uploaded_file_' . time();

$this->assertEquals(true, $tmpImage->saveAs($targetFile, $deleteTempFile = false));
$this->assertEquals(true, $tmpImage->saveAs($targetFile, $deleteTempFile = true));
$this->assertEquals(false, $tmpImage->saveAs($targetFile)); // temp file should not exist

@unlink($targetFile);
}

public function testSaveFileFromMultipartFormDataParser()
{
$_FILES = [];
UploadedFile::reset();
$model = new ModelStub();
$targetFile = '@runtime/test_saved_uploaded_file_' . time();

(new MultipartFormDataParserTest)->testParse();
$_FILES['ModelStub'] = $_FILES['Item']; // $_FILES[Item] here from testParse() above
$tmpFile = UploadedFile::getInstance($model, 'file');

$this->assertEquals(true, $tmpFile->saveAs($targetFile));
@unlink($targetFile);
}
}
19 changes: 19 additions & 0 deletions tests/framework/web/mocks/UploadedFileMock.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
<?php
/**
* @link http://www.yiiframework.com/
* @copyright Copyright (c) 2008 Yii Software LLC
* @license http://www.yiiframework.com/license/
*/

namespace yiiunit\framework\web\mocks;

class UploadedFileMock extends \yii\web\UploadedFile
{
/**
* @inheritDoc
*/
protected function isUploadedFile($file)
{
return is_file($file); // is_uploaded_file() won't work in test
}
}