Browse Source

Fix #18676: Added method `yii\helpers\BaseFileHelper::changeOwnership()` and properties `newFileMode`/`newFileOwnership` in `yii\console\controllers\BaseMigrateController`

Co-authored-by: Bizley <pawel@positive.codes>
tags/2.0.43
rhertogh 3 years ago committed by GitHub
parent
commit
31ca0fcb6f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 16
      docs/guide/db-migrations.md
  2. 2
      framework/CHANGELOG.md
  3. 16
      framework/console/controllers/BaseMigrateController.php
  4. 80
      framework/helpers/BaseFileHelper.php
  5. 306
      tests/framework/helpers/FileHelperTest.php

16
docs/guide/db-migrations.md

@ -187,6 +187,22 @@ class m150101_185401_create_news_table extends Migration
A list of all available methods for defining the column types is available in the API documentation of [[yii\db\SchemaBuilderTrait]].
> Info: The generated file permissions and ownership will be determined by the current environment. This might lead to
inaccessible files. This could, for example, happen when the migration is created within a docker container
and the files are edited on the host. In this case the `newFileMode` and/or `newFileOwnership` of the MigrateController
can be changed. E.g. in the application config:
```php
<?php
return [
'controllerMap' => [
'migrate' => [
'class' => 'yii\console\controllers\MigrateController',
'newFileOwnership' => '1000:1000', # Default WSL user id
'newFileMode' => 0660,
],
],
];
```
## Generating Migrations <span id="generating-migrations"></span>

2
framework/CHANGELOG.md

@ -8,8 +8,10 @@ Yii Framework 2 Change Log
- Enh #18628: Added strings "software", and "hardware" to `$specials` array in `yii\helpers\BaseInflector` (kjusupov)
- Enh #18653: Added method `yii\helpers\BaseHtml::getInputIdByName()` (WinterSilence)
- Enh #18669: Changed visibility of `yii\web\User::checkRedirectAcceptable()` to `public` (rhertogh)
- Enh #18676: Added method `yii\helpers\BaseFileHelper::changeOwnership()` and properties `newFileMode`/`newFileOwnership` in `yii\console\controllers\BaseMigrateController` (rhertogh)
- Bug #18678: Fix `yii\caching\DbCache` to use configured cache table name instead of the default one in case of MSSQL varbinary column type detection (aidanbek)
2.0.42.1 May 06, 2021
---------------------

16
framework/console/controllers/BaseMigrateController.php

@ -85,6 +85,20 @@ abstract class BaseMigrateController extends Controller
*/
public $templateFile;
/**
* @var int the permission to be set for newly generated migration files.
* This value will be used by PHP chmod() function. No umask will be applied.
* If not set, the permission will be determined by the current environment.
* @since 2.0.43
*/
public $newFileMode;
/**
* @var string|int the user and/or group ownership to be set for newly generated migration files.
* If not set, the ownership will be determined by the current environment.
* @since 2.0.43
* @see FileHelper::changeOwnership()
*/
public $newFileOwnership;
/**
* @var bool indicates whether the console output should be compacted.
* If this is set to true, the individual commands ran within the migration will not be output to the console.
* Default is false, in other words the output is fully verbose by default.
@ -663,6 +677,8 @@ abstract class BaseMigrateController extends Controller
return ExitCode::IOERR;
}
FileHelper::changeOwnership($file, $this->newFileOwnership, $this->newFileMode);
$this->stdout("New migration created successfully.\n", Console::FG_GREEN);
}

80
framework/helpers/BaseFileHelper.php

@ -9,6 +9,7 @@ namespace yii\helpers;
use Yii;
use yii\base\ErrorException;
use yii\base\Exception;
use yii\base\InvalidArgumentException;
use yii\base\InvalidConfigException;
@ -874,4 +875,83 @@ class BaseFileHelper
return $options;
}
/**
* Changes the Unix user and/or group ownership of a file or directory, and optionally the mode.
* Note: This function will not work on remote files as the file to be examined must be accessible
* via the server's filesystem.
* Note: On Windows, this function fails silently when applied on a regular file.
* @param string $path the path to the file or directory.
* @param string|array|int|null $ownership the user and/or group ownership for the file or directory.
* When $ownership is a string, the format is 'user:group' where both are optional. E.g.
* 'user' or 'user:' will only change the user,
* ':group' will only change the group,
* 'user:group' will change both.
* When $owners is an index array the format is [0 => user, 1 => group], e.g. `[$myUser, $myGroup]`.
* It is also possible to pass an associative array, e.g. ['user' => $myUser, 'group' => $myGroup].
* In case $owners is an integer it will be used as user id.
* If `null`, an empty array or an empty string is passed, the ownership will not be changed.
* @param int|null $mode the permission to be set for the file or directory.
* If `null` is passed, the mode will not be changed.
*
* @since 2.0.43
*/
public static function changeOwnership($path, $ownership, $mode = null)
{
if (!file_exists($path)) {
throw new InvalidArgumentException('Unable to change ownerhip, "' . $path . '" is not a file or directory.');
}
if (empty($ownership) && $ownership !== 0 && $mode === null) {
return;
}
$user = $group = null;
if (!empty($ownership) || $ownership === 0 || $ownership === '0') {
if (is_int($ownership)) {
$user = $ownership;
} elseif (is_string($ownership)) {
$ownerParts = explode(':', $ownership);
$user = $ownerParts[0];
if (count($ownerParts) > 1) {
$group = $ownerParts[1];
}
} elseif (is_array($ownership)) {
$ownershipIsIndexed = ArrayHelper::isIndexed($ownership);
$user = ArrayHelper::getValue($ownership, $ownershipIsIndexed ? 0 : 'user');
$group = ArrayHelper::getValue($ownership, $ownershipIsIndexed ? 1 : 'group');
} else {
throw new InvalidArgumentException('$ownership must be an integer, string, array, or null.');
}
}
if ($mode !== null) {
if (!is_int($mode)) {
throw new InvalidArgumentException('$mode must be an integer or null.');
}
if (!chmod($path, $mode)) {
throw new Exception('Unable to change mode of "' . $path . '" to "0' . decoct($mode) . '".');
}
}
if ($user !== null && $user !== '') {
if (is_numeric($user)) {
$user = (int) $user;
} elseif (!is_string($user)) {
throw new InvalidArgumentException('The user part of $ownership must be an integer, string, or null.');
}
if (!chown($path, $user)) {
throw new Exception('Unable to change user ownership of "' . $path . '" to "' . $user . '".');
}
}
if ($group !== null && $group !== '') {
if (is_numeric($group)) {
$group = (int) $group;
} elseif (!is_string($group)) {
throw new InvalidArgumentException('The group part of $ownership must be an integer, string or null.');
}
if (!chgrp($path, $group)) {
throw new Exception('Unable to change group ownership of "' . $path . '" to "' . $group . '".');
}
}
}
}

306
tests/framework/helpers/FileHelperTest.php

@ -6,6 +6,7 @@
*/
use yii\helpers\FileHelper;
use yii\helpers\VarDumper;
use yiiunit\TestCase;
/**
@ -937,4 +938,309 @@ class FileHelperTest extends TestCase
sort($foundFiles);
$this->assertEquals($expectedFiles, $foundFiles);
}
public function testChangeOwnership()
{
if (DIRECTORY_SEPARATOR !== '/') {
$this->markTestSkipped('FileHelper::changeOwnership() fails silently on Windows, nothing to test.');
}
if (!extension_loaded('posix')) {
$this->markTestSkipped('posix extension is required.');
}
$dirName = 'change_ownership_test_dir';
$fileName = 'file_1.txt';
$testFile = $this->testFilePath . DIRECTORY_SEPARATOR . $dirName . DIRECTORY_SEPARATOR . $fileName;
$currentUserId = posix_getuid();
$currentUserName = posix_getpwuid($currentUserId)['name'];
$currentGroupId = posix_getgid();
$currentGroupName = posix_getgrgid($currentGroupId)['name'];
/////////////
/// Setup ///
/////////////
$this->createFileStructure([
$dirName => [
$fileName => 'test 1',
],
]);
// Ensure the test file is created as the current user/group and has a specific file mode
$this->assertFileExists($testFile);
$fileMode = 0770;
@chmod($testFile, $fileMode);
clearstatcache(true, $testFile);
$this->assertEquals($currentUserId, fileowner($testFile), 'Expected created test file owner to be current user.');
$this->assertEquals($currentGroupId, filegroup($testFile), 'Expected created test file group to be current group.');
$this->assertEquals('0'.decoct($fileMode), substr(decoct(fileperms($testFile)), -4), 'Expected file mode to be changed.');
/////////////////
/// File Mode ///
/////////////////
// Test file mode
$fileMode = 0777;
FileHelper::changeOwnership($testFile, null, $fileMode);
clearstatcache(true, $testFile);
$this->assertEquals($currentUserId, fileowner($testFile), 'Expected file owner to be unchanged.');
$this->assertEquals($currentGroupId, filegroup($testFile), 'Expected file group to be unchanged.');
$this->assertEquals('0'.decoct($fileMode), substr(decoct(fileperms($testFile)), -4), 'Expected file mode to be changed.');
if ($currentUserId !== 0) {
$this->markTestInComplete(__METHOD__ . ' could only run partially, chown() can only to be tested as root user. Current user: ' . $currentUserName);
}
//////////////////////
/// User Ownership ///
//////////////////////
// Test user ownership as integer
$ownership = 10001;
FileHelper::changeOwnership($testFile, $ownership);
clearstatcache(true, $testFile);
$this->assertEquals($ownership, fileowner($testFile), 'Expected file owner to be changed.');
$this->assertEquals($currentGroupId, filegroup($testFile), 'Expected file group to be unchanged.');
$this->assertEquals('0'.decoct($fileMode), substr(decoct(fileperms($testFile)), -4), 'Expected file mode to be unchanged.');
// Test user ownership as numeric string (should be treated as integer)
$ownership = '10002';
FileHelper::changeOwnership($testFile, $ownership);
clearstatcache(true, $testFile);
$this->assertEquals((int)$ownership, fileowner($testFile), 'Expected created test file owner to be changed.');
$this->assertEquals($currentGroupId, filegroup($testFile), 'Expected file group to be unchanged.');
$this->assertEquals('0'.decoct($fileMode), substr(decoct(fileperms($testFile)), -4), 'Expected file mode to be unchanged.');
// Test user ownership as string
$ownership = $currentUserName;
FileHelper::changeOwnership($testFile, $ownership);
clearstatcache(true, $testFile);
$this->assertEquals($ownership, posix_getpwuid(fileowner($testFile))['name'], 'Expected created test file owner to be changed.');
$this->assertEquals($currentGroupId, filegroup($testFile), 'Expected file group to be unchanged.');
$this->assertEquals('0'.decoct($fileMode), substr(decoct(fileperms($testFile)), -4), 'Expected file mode to be unchanged.');
// Test user ownership as numeric string with trailing colon (should be treated as integer)
$ownership = '10003:';
FileHelper::changeOwnership($testFile, $ownership);
clearstatcache(true, $testFile);
$this->assertEquals((int)$ownership, fileowner($testFile), 'Expected created test file owner to be changed.');
$this->assertEquals($currentGroupId, filegroup($testFile), 'Expected file group to be unchanged.');
$this->assertEquals('0'.decoct($fileMode), substr(decoct(fileperms($testFile)), -4), 'Expected file mode to be unchanged.');
// Test user ownership as string with trailing colon
$ownership = $currentUserName . ':';
FileHelper::changeOwnership($testFile, $ownership);
clearstatcache(true, $testFile);
$this->assertEquals(substr($ownership, 0, -1), posix_getpwuid(fileowner($testFile))['name'], 'Expected created test file owner to be changed.');
$this->assertEquals($currentGroupId, filegroup($testFile), 'Expected file group to be unchanged.');
$this->assertEquals('0'.decoct($fileMode), substr(decoct(fileperms($testFile)), -4), 'Expected file mode to be unchanged.');
// Test user ownership as indexed array (integer value)
$ownership = [10004];
FileHelper::changeOwnership($testFile, $ownership);
clearstatcache(true, $testFile);
$this->assertEquals($ownership[0], fileowner($testFile), 'Expected created test file owner to be changed.');
$this->assertEquals($currentGroupId, filegroup($testFile), 'Expected file group to be unchanged.');
$this->assertEquals('0'.decoct($fileMode), substr(decoct(fileperms($testFile)), -4), 'Expected file mode to be unchanged.');
// Test user ownership as indexed array (numeric string value)
$ownership = ['10005'];
FileHelper::changeOwnership($testFile, $ownership);
clearstatcache(true, $testFile);
$this->assertEquals((int)$ownership[0], fileowner($testFile), 'Expected created test file owner to be changed.');
$this->assertEquals($currentGroupId, filegroup($testFile), 'Expected file group to be unchanged.');
$this->assertEquals('0'.decoct($fileMode), substr(decoct(fileperms($testFile)), -4), 'Expected file mode to be unchanged.');
// Test user ownership as associative array (string value)
$ownership = ['user' => $currentUserName];
FileHelper::changeOwnership($testFile, $ownership);
clearstatcache(true, $testFile);
$this->assertEquals($ownership['user'], posix_getpwuid(fileowner($testFile))['name'], 'Expected created test file owner to be changed.');
$this->assertEquals($currentGroupId, filegroup($testFile), 'Expected file group to be unchanged.');
$this->assertEquals('0'.decoct($fileMode), substr(decoct(fileperms($testFile)), -4), 'Expected file mode to be unchanged.');
///////////////////////
/// Group Ownership ///
///////////////////////
// Test group ownership as numeric string
$ownership = ':10006';
FileHelper::changeOwnership($testFile, $ownership);
clearstatcache(true, $testFile);
$this->assertEquals($currentUserId, fileowner($testFile), 'Expected file owner to be unchanged.');
$this->assertEquals((int)substr($ownership, 1), filegroup($testFile), 'Expected created test file group to be changed.');
$this->assertEquals('0'.decoct($fileMode), substr(decoct(fileperms($testFile)), -4), 'Expected file mode to be unchanged.');
// Test group ownership as string
$ownership = ':' . $currentGroupName;
FileHelper::changeOwnership($testFile, $ownership);
clearstatcache(true, $testFile);
$this->assertEquals($currentUserId, fileowner($testFile), 'Expected file owner to be unchanged.');
$this->assertEquals(substr($ownership, 1), posix_getgrgid(filegroup($testFile))['name'], 'Expected created test file group to be changed.');
$this->assertEquals('0'.decoct($fileMode), substr(decoct(fileperms($testFile)), -4), 'Expected file mode to be unchanged.');
// Test group ownership as associative array (integer value)
$ownership = ['group' => 10007];
FileHelper::changeOwnership($testFile, $ownership);
clearstatcache(true, $testFile);
$this->assertEquals($currentUserId, fileowner($testFile), 'Expected file owner to be unchanged.');
$this->assertEquals($ownership['group'], filegroup($testFile), 'Expected created test file group to be changed.');
$this->assertEquals('0'.decoct($fileMode), substr(decoct(fileperms($testFile)), -4), 'Expected file mode to be unchanged.');
// Test group ownership as associative array (numeric string value)
$ownership = ['group' => '10008'];
FileHelper::changeOwnership($testFile, $ownership);
clearstatcache(true, $testFile);
$this->assertEquals($currentUserId, fileowner($testFile), 'Expected file owner to be unchanged.');
$this->assertEquals((int)$ownership['group'], filegroup($testFile), 'Expected created test file group to be changed.');
$this->assertEquals('0'.decoct($fileMode), substr(decoct(fileperms($testFile)), -4), 'Expected file mode to be unchanged.');
// Test group ownership as associative array (string value)
$ownership = ['group' => $currentGroupName];
FileHelper::changeOwnership($testFile, $ownership);
clearstatcache(true, $testFile);
$this->assertEquals($currentUserId, fileowner($testFile), 'Expected file owner to be unchanged.');
$this->assertEquals($ownership['group'], posix_getgrgid(filegroup($testFile))['name'], 'Expected created test file group to be changed.');
$this->assertEquals('0'.decoct($fileMode), substr(decoct(fileperms($testFile)), -4), 'Expected file mode to be unchanged.');
/////////////////////////////////
/// User- and Group Ownership ///
/////////////////////////////////
// Test user and group ownership as numeric string
$ownership = '10009:10010';
FileHelper::changeOwnership($testFile, $ownership);
clearstatcache(true, $testFile);
$this->assertEquals((int)explode(':', $ownership)[0], fileowner($testFile), 'Expected file owner to be changed.');
$this->assertEquals((int)explode(':', $ownership)[1], filegroup($testFile), 'Expected created test file group to be changed.');
$this->assertEquals('0'.decoct($fileMode), substr(decoct(fileperms($testFile)), -4), 'Expected file mode to be unchanged.');
// Test user and group ownership as string
$ownership = $currentUserName . ':' . $currentGroupName;
FileHelper::changeOwnership($testFile, $ownership);
clearstatcache(true, $testFile);
$this->assertEquals(explode(':', $ownership)[0], posix_getpwuid(fileowner($testFile))['name'], 'Expected file owner to be changed.');
$this->assertEquals(explode(':', $ownership)[1], posix_getgrgid(filegroup($testFile))['name'], 'Expected created test file group to be changed.');
$this->assertEquals('0'.decoct($fileMode), substr(decoct(fileperms($testFile)), -4), 'Expected file mode to be unchanged.');
// Test user and group ownership as indexed array (integer values)
$ownership = [10011, 10012];
FileHelper::changeOwnership($testFile, $ownership);
clearstatcache(true, $testFile);
$this->assertEquals($ownership[0], fileowner($testFile), 'Expected file owner to be changed.');
$this->assertEquals($ownership[1], filegroup($testFile), 'Expected created test file group to be changed.');
$this->assertEquals('0'.decoct($fileMode), substr(decoct(fileperms($testFile)), -4), 'Expected file mode to be unchanged.');
// Test user and group ownership as indexed array (numeric string values)
$ownership = ['10013', '10014'];
FileHelper::changeOwnership($testFile, $ownership);
clearstatcache(true, $testFile);
$this->assertEquals((int)$ownership[0], fileowner($testFile), 'Expected file owner to be changed.');
$this->assertEquals((int)$ownership[1], filegroup($testFile), 'Expected created test file group to be changed.');
$this->assertEquals('0'.decoct($fileMode), substr(decoct(fileperms($testFile)), -4), 'Expected file mode to be unchanged.');
// Test user and group ownership as indexed array (string values)
$ownership = [$currentUserName, $currentGroupName];
FileHelper::changeOwnership($testFile, $ownership);
clearstatcache(true, $testFile);
$this->assertEquals($ownership[0], posix_getpwuid(fileowner($testFile))['name'], 'Expected file owner to be changed.');
$this->assertEquals($ownership[1], posix_getgrgid(filegroup($testFile))['name'], 'Expected created test file group to be changed.');
$this->assertEquals('0'.decoct($fileMode), substr(decoct(fileperms($testFile)), -4), 'Expected file mode to be unchanged.');
// Test user and group ownership as associative array (integer values)
$ownership = ['group' => 10015, 'user' => 10016]; // user/group reversed on purpose
FileHelper::changeOwnership($testFile, $ownership);
clearstatcache(true, $testFile);
$this->assertEquals($ownership['user'], fileowner($testFile), 'Expected file owner to be changed.');
$this->assertEquals($ownership['group'], filegroup($testFile), 'Expected created test file group to be changed.');
$this->assertEquals('0'.decoct($fileMode), substr(decoct(fileperms($testFile)), -4), 'Expected file mode to be unchanged.');
// Test user and group ownership as associative array (numeric string values)
$ownership = ['group' => '10017', 'user' => '10018']; // user/group reversed on purpose
FileHelper::changeOwnership($testFile, $ownership);
clearstatcache(true, $testFile);
$this->assertEquals((int)$ownership['user'], fileowner($testFile), 'Expected file owner to be changed.');
$this->assertEquals((int)$ownership['group'], filegroup($testFile), 'Expected created test file group to be changed.');
$this->assertEquals('0'.decoct($fileMode), substr(decoct(fileperms($testFile)), -4), 'Expected file mode to be unchanged.');
// Test user and group ownership as associative array (string values)
$ownership = ['group' => $currentGroupName, 'user' => $currentUserName]; // user/group reversed on purpose
FileHelper::changeOwnership($testFile, $ownership);
clearstatcache(true, $testFile);
$this->assertEquals($ownership['user'], posix_getpwuid(fileowner($testFile))['name'], 'Expected file owner to be changed.');
$this->assertEquals($ownership['group'], posix_getgrgid(filegroup($testFile))['name'], 'Expected created test file group to be changed.');
$this->assertEquals('0'.decoct($fileMode), substr(decoct(fileperms($testFile)), -4), 'Expected file mode to be unchanged.');
///////////////////////////////////////
/// Mode, User- and Group Ownership ///
///////////////////////////////////////
// Test user ownership as integer with file mode
$ownership = '10019:10020';
$fileMode = 0774;
FileHelper::changeOwnership($testFile, $ownership, $fileMode);
clearstatcache(true, $testFile);
$this->assertEquals(explode(':', $ownership)[0], fileowner($testFile), 'Expected created test file owner to be changed.');
$this->assertEquals(explode(':', $ownership)[1], filegroup($testFile), 'Expected file group to be unchanged.');
$this->assertEquals('0'.decoct($fileMode), substr(decoct(fileperms($testFile)), -4), 'Expected created test file mode to be changed.');
}
public function testChangeOwnershipNonExistingUser()
{
$dirName = 'change_ownership_non_existing_user';
$fileName = 'file_1.txt';
$testFile = $this->testFilePath . DIRECTORY_SEPARATOR . $dirName . DIRECTORY_SEPARATOR . $fileName;
$this->createFileStructure([
$dirName => [
$fileName => 'test 1',
],
]);
// Test user ownership as integer with file mode (Due to the nature of chown we can't use PHPUnit's `expectException`)
$ownership = 'non_existing_user';
try {
FileHelper::changeOwnership($testFile, $ownership);
throw new \Exception('FileHelper::changeOwnership() should have thrown error for non existing user.');
} catch(\Exception $e) {
$this->assertEquals('chown(): Unable to find uid for non_existing_user', $e->getMessage());
}
}
/**
* @dataProvider changeOwnershipInvalidArgumentsProvider
* @param bool $useFile
* @param mixed $ownership
* @param mixed $mode
*/
public function testChangeOwnershipInvalidArguments($useFile, $ownership, $mode)
{
$dirName = 'change_ownership_invalid_arguments';
$fileName = 'file_1.txt';
$file = $this->testFilePath . DIRECTORY_SEPARATOR . $dirName . DIRECTORY_SEPARATOR . $fileName;
$this->createFileStructure([
$dirName => [
$fileName => 'test 1',
],
]);
$this->expectException('yii\base\InvalidArgumentException');
FileHelper::changeOwnership($useFile ? $file : null, $ownership, $mode);
}
public function changeOwnershipInvalidArgumentsProvider()
{
return [
[false, '123:123', null],
[true, new stdClass(), null],
[true, ['user' => new stdClass()], null],
[true, ['group' => new stdClass()], null],
[true, null, 'test'],
];
}
}

Loading…
Cancel
Save