aboutsummaryrefslogtreecommitdiffstats
path: root/apps/files_sharing/tests/SharedMountTest.php
diff options
context:
space:
mode:
Diffstat (limited to 'apps/files_sharing/tests/SharedMountTest.php')
-rw-r--r--apps/files_sharing/tests/SharedMountTest.php394
1 files changed, 186 insertions, 208 deletions
diff --git a/apps/files_sharing/tests/SharedMountTest.php b/apps/files_sharing/tests/SharedMountTest.php
index 1eddbcb64f7..cc9c70a241f 100644
--- a/apps/files_sharing/tests/SharedMountTest.php
+++ b/apps/files_sharing/tests/SharedMountTest.php
@@ -1,35 +1,24 @@
<?php
+
/**
- * @copyright Copyright (c) 2016, ownCloud, Inc.
- *
- * @author Björn Schießle <bjoern@schiessle.org>
- * @author Joas Schilling <coding@schilljs.com>
- * @author Lukas Reschke <lukas@statuscode.ch>
- * @author Morris Jobke <hey@morrisjobke.de>
- * @author Robin Appelman <robin@icewind.nl>
- * @author Roeland Jago Douma <roeland@famdouma.nl>
- * @author Thomas Müller <thomas.mueller@tmit.eu>
- * @author Vincent Petry <pvince81@owncloud.com>
- *
- * @license AGPL-3.0
- *
- * This code is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License, version 3,
- * as published by the Free Software Foundation.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License, version 3,
- * along with this program. If not, see <http://www.gnu.org/licenses/>
- *
+ * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-FileCopyrightText: 2016 ownCloud, Inc.
+ * SPDX-License-Identifier: AGPL-3.0-only
*/
-
namespace OCA\Files_Sharing\Tests;
+
+use OC\Files\Filesystem;
+use OC\Files\View;
+use OC\Memcache\ArrayCache;
+use OCA\Files_Sharing\MountProvider;
+use OCA\Files_Sharing\SharedMount;
+use OCP\Constants;
+use OCP\ICacheFactory;
+use OCP\IDBConnection;
use OCP\IGroupManager;
use OCP\IUserManager;
+use OCP\Server;
+use OCP\Share\IShare;
/**
* Class SharedMountTest
@@ -44,25 +33,30 @@ class SharedMountTest extends TestCase {
/** @var IUserManager */
private $userManager;
- protected function setUp() {
+ private $folder2;
+
+ protected function setUp(): void {
parent::setUp();
$this->folder = '/folder_share_storage_test';
+ $this->folder2 = '/folder_share_storage_test2';
$this->filename = '/share-api-storage.txt';
$this->view->mkdir($this->folder);
+ $this->view->mkdir($this->folder2);
// save file with content
$this->view->file_put_contents($this->filename, 'root file');
$this->view->file_put_contents($this->folder . $this->filename, 'file in subfolder');
+ $this->view->file_put_contents($this->folder2 . $this->filename, 'file in subfolder2');
- $this->groupManager = \OC::$server->getGroupManager();
- $this->userManager = \OC::$server->getUserManager();
+ $this->groupManager = Server::get(IGroupManager::class);
+ $this->userManager = Server::get(IUserManager::class);
}
- protected function tearDown() {
+ protected function tearDown(): void {
if ($this->view) {
if ($this->view->file_exists($this->folder)) {
$this->view->unlink($this->folder);
@@ -78,15 +72,16 @@ class SharedMountTest extends TestCase {
/**
* test if the mount point moves up if the parent folder no longer exists
*/
- public function testShareMountLoseParentFolder() {
+ public function testShareMountLoseParentFolder(): void {
// share to user
$share = $this->share(
- \OCP\Share::SHARE_TYPE_USER,
+ IShare::TYPE_USER,
$this->folder,
self::TEST_FILES_SHARING_API_USER1,
self::TEST_FILES_SHARING_API_USER2,
- \OCP\Constants::PERMISSION_ALL);
+ Constants::PERMISSION_ALL);
+ $this->shareManager->acceptShare($share, self::TEST_FILES_SHARING_API_USER2);
$share->setTarget('/foo/bar' . $this->folder);
$this->shareManager->moveShare($share, self::TEST_FILES_SHARING_API_USER2);
@@ -109,18 +104,18 @@ class SharedMountTest extends TestCase {
/**
* @medium
*/
- public function testDeleteParentOfMountPoint() {
+ public function testDeleteParentOfMountPoint(): void {
// share to user
$share = $this->share(
- \OCP\Share::SHARE_TYPE_USER,
+ IShare::TYPE_USER,
$this->folder,
self::TEST_FILES_SHARING_API_USER1,
self::TEST_FILES_SHARING_API_USER2,
- \OCP\Constants::PERMISSION_ALL
+ Constants::PERMISSION_ALL
);
self::loginHelper(self::TEST_FILES_SHARING_API_USER2);
- $user2View = new \OC\Files\View('/' . self::TEST_FILES_SHARING_API_USER2 . '/files');
+ $user2View = new View('/' . self::TEST_FILES_SHARING_API_USER2 . '/files');
$this->assertTrue($user2View->file_exists($this->folder));
// create a local folder
@@ -149,31 +144,31 @@ class SharedMountTest extends TestCase {
$this->view->unlink($this->folder);
}
- public function testMoveSharedFile() {
+ public function testMoveSharedFile(): void {
$share = $this->share(
- \OCP\Share::SHARE_TYPE_USER,
+ IShare::TYPE_USER,
$this->filename,
self::TEST_FILES_SHARING_API_USER1,
self::TEST_FILES_SHARING_API_USER2,
- \OCP\Constants::PERMISSION_READ | \OCP\Constants::PERMISSION_UPDATE | \OCP\Constants::PERMISSION_SHARE
+ Constants::PERMISSION_READ | Constants::PERMISSION_UPDATE | Constants::PERMISSION_SHARE
);
self::loginHelper(self::TEST_FILES_SHARING_API_USER2);
- \OC\Files\Filesystem::rename($this->filename, $this->filename . '_renamed');
+ Filesystem::rename($this->filename, $this->filename . '_renamed');
- $this->assertTrue(\OC\Files\Filesystem::file_exists($this->filename . '_renamed'));
- $this->assertFalse(\OC\Files\Filesystem::file_exists($this->filename));
+ $this->assertTrue(Filesystem::file_exists($this->filename . '_renamed'));
+ $this->assertFalse(Filesystem::file_exists($this->filename));
self::loginHelper(self::TEST_FILES_SHARING_API_USER1);
- $this->assertTrue(\OC\Files\Filesystem::file_exists($this->filename));
- $this->assertFalse(\OC\Files\Filesystem::file_exists($this->filename . '_renamed'));
+ $this->assertTrue(Filesystem::file_exists($this->filename));
+ $this->assertFalse(Filesystem::file_exists($this->filename . '_renamed'));
// rename back to original name
self::loginHelper(self::TEST_FILES_SHARING_API_USER2);
- \OC\Files\Filesystem::rename($this->filename . '_renamed', $this->filename);
- $this->assertFalse(\OC\Files\Filesystem::file_exists($this->filename . '_renamed'));
- $this->assertTrue(\OC\Files\Filesystem::file_exists($this->filename));
+ Filesystem::rename($this->filename . '_renamed', $this->filename);
+ $this->assertFalse(Filesystem::file_exists($this->filename . '_renamed'));
+ $this->assertTrue(Filesystem::file_exists($this->filename));
//cleanup
$this->shareManager->deleteShare($share);
@@ -183,7 +178,7 @@ class SharedMountTest extends TestCase {
* share file with a group if a user renames the file the filename should not change
* for the other users
*/
- public function testMoveGroupShare () {
+ public function testMoveGroupShare(): void {
$testGroup = $this->groupManager->createGroup('testGroup');
$user1 = $this->userManager->get(self::TEST_FILES_SHARING_API_USER1);
$user2 = $this->userManager->get(self::TEST_FILES_SHARING_API_USER2);
@@ -194,29 +189,32 @@ class SharedMountTest extends TestCase {
$fileinfo = $this->view->getFileInfo($this->filename);
$share = $this->share(
- \OCP\Share::SHARE_TYPE_GROUP,
+ IShare::TYPE_GROUP,
$this->filename,
self::TEST_FILES_SHARING_API_USER1,
'testGroup',
- \OCP\Constants::PERMISSION_READ | \OCP\Constants::PERMISSION_UPDATE | \OCP\Constants::PERMISSION_SHARE
+ Constants::PERMISSION_READ | Constants::PERMISSION_UPDATE | Constants::PERMISSION_SHARE
);
+ $this->shareManager->acceptShare($share, $user1->getUID());
+ $this->shareManager->acceptShare($share, $user2->getUID());
+ $this->shareManager->acceptShare($share, $user3->getUID());
self::loginHelper(self::TEST_FILES_SHARING_API_USER2);
- $this->assertTrue(\OC\Files\Filesystem::file_exists($this->filename));
+ $this->assertTrue(Filesystem::file_exists($this->filename));
- \OC\Files\Filesystem::rename($this->filename, 'newFileName');
+ Filesystem::rename($this->filename, 'newFileName');
- $this->assertTrue(\OC\Files\Filesystem::file_exists('newFileName'));
- $this->assertFalse(\OC\Files\Filesystem::file_exists($this->filename));
+ $this->assertTrue(Filesystem::file_exists('newFileName'));
+ $this->assertFalse(Filesystem::file_exists($this->filename));
self::loginHelper(self::TEST_FILES_SHARING_API_USER3);
- $this->assertTrue(\OC\Files\Filesystem::file_exists($this->filename));
- $this->assertFalse(\OC\Files\Filesystem::file_exists('newFileName'));
+ $this->assertTrue(Filesystem::file_exists($this->filename));
+ $this->assertFalse(Filesystem::file_exists('newFileName'));
self::loginHelper(self::TEST_FILES_SHARING_API_USER3);
- $this->assertTrue(\OC\Files\Filesystem::file_exists($this->filename));
- $this->assertFalse(\OC\Files\Filesystem::file_exists('newFileName'));
+ $this->assertTrue(Filesystem::file_exists($this->filename));
+ $this->assertFalse(Filesystem::file_exists('newFileName'));
//cleanup
self::loginHelper(self::TEST_FILES_SHARING_API_USER1);
@@ -227,12 +225,12 @@ class SharedMountTest extends TestCase {
}
/**
- * @dataProvider dataProviderTestStripUserFilesPath
* @param string $path
* @param string $expectedResult
* @param bool $exception if a exception is expected
*/
- public function testStripUserFilesPath($path, $expectedResult, $exception) {
+ #[\PHPUnit\Framework\Attributes\DataProvider('dataProviderTestStripUserFilesPath')]
+ public function testStripUserFilesPath($path, $expectedResult, $exception): void {
$testClass = new DummyTestClassSharedMount(null, null);
try {
$result = $testClass->stripUserFilesPathDummy($path);
@@ -241,97 +239,27 @@ class SharedMountTest extends TestCase {
if ($exception) {
$this->assertSame(10, $e->getCode());
} else {
- $this->assertTrue(false, 'Exception catched, but expected: ' . $expectedResult);
+ $this->assertTrue(false, 'Exception caught, but expected: ' . $expectedResult);
}
}
}
- public function dataProviderTestStripUserFilesPath() {
- return array(
- array('/user/files/foo.txt', '/foo.txt', false),
- array('/user/files/folder/foo.txt', '/folder/foo.txt', false),
- array('/data/user/files/foo.txt', null, true),
- array('/data/user/files/', null, true),
- array('/files/foo.txt', null, true),
- array('/foo.txt', null, true),
- );
- }
-
- public function dataPermissionMovedGroupShare() {
- $data = [];
-
- $powerset = function($permissions) {
- $results = [\OCP\Constants::PERMISSION_READ];
-
- foreach ($permissions as $permission) {
- foreach ($results as $combination) {
- $results[] = $permission | $combination;
- }
- }
- return $results;
- };
-
- //Generate file permissions
- $permissions = [
- \OCP\Constants::PERMISSION_UPDATE,
- \OCP\Constants::PERMISSION_SHARE,
+ public static function dataProviderTestStripUserFilesPath() {
+ return [
+ ['/user/files/foo.txt', '/foo.txt', false],
+ ['/user/files/folder/foo.txt', '/folder/foo.txt', false],
+ ['/data/user/files/foo.txt', null, true],
+ ['/data/user/files/', null, true],
+ ['/files/foo.txt', null, true],
+ ['/foo.txt', null, true],
];
-
- $allPermissions = $powerset($permissions);
-
- foreach ($allPermissions as $before) {
- foreach ($allPermissions as $after) {
- if ($before === $after) { continue; }
-
- $data[] = [
- 'file',
- $before,
- $after,
- ];
- }
- }
-
- //Generate folder permissions
- $permissions = [
- \OCP\Constants::PERMISSION_UPDATE,
- \OCP\Constants::PERMISSION_CREATE,
- \OCP\Constants::PERMISSION_SHARE,
- \OCP\Constants::PERMISSION_DELETE,
- ];
-
- $allPermissions = $powerset($permissions);
-
- foreach ($allPermissions as $before) {
- foreach ($allPermissions as $after) {
- if ($before === $after) { continue; }
-
- $data[] = [
- 'folder',
- $before,
- $after,
- ];
- }
- }
-
- return $data;
}
-
-
/**
- * moved mountpoints of a group share should keep the same permission as their parent group share.
- * See #15253
- *
- * @dataProvider dataPermissionMovedGroupShare
+ * If the permissions on a group share are upgraded be sure to still respect
+ * removed shares by a member of that group
*/
- public function testPermissionMovedGroupShare($type, $beforePerm, $afterPerm) {
-
- if ($type === 'file') {
- $path = $this->filename;
- } else if ($type === 'folder') {
- $path = $this->folder;
- }
-
+ public function testPermissionUpgradeOnUserDeletedGroupShare(): void {
$testGroup = $this->groupManager->createGroup('testGroup');
$user1 = $this->userManager->get(self::TEST_FILES_SHARING_API_USER1);
$user2 = $this->userManager->get(self::TEST_FILES_SHARING_API_USER2);
@@ -340,115 +268,165 @@ class SharedMountTest extends TestCase {
$testGroup->addUser($user2);
$testGroup->addUser($user3);
+ $connection = Server::get(IDBConnection::class);
+
// Share item with group
+ $fileinfo = $this->view->getFileInfo($this->folder);
$share = $this->share(
- \OCP\Share::SHARE_TYPE_GROUP,
- $path,
+ IShare::TYPE_GROUP,
+ $this->folder,
self::TEST_FILES_SHARING_API_USER1,
'testGroup',
- $beforePerm
+ Constants::PERMISSION_READ
);
+ $this->shareManager->acceptShare($share, $user1->getUID());
+ $this->shareManager->acceptShare($share, $user2->getUID());
+ $this->shareManager->acceptShare($share, $user3->getUID());
// Login as user 2 and verify the item exists
self::loginHelper(self::TEST_FILES_SHARING_API_USER2);
- $this->assertTrue(\OC\Files\Filesystem::file_exists($path));
+ $this->assertTrue(Filesystem::file_exists($this->folder));
$result = $this->shareManager->getShareById($share->getFullId(), self::TEST_FILES_SHARING_API_USER2);
- $this->assertEquals($beforePerm, $result->getPermissions());
+ $this->assertNotEmpty($result);
+ $this->assertEquals(Constants::PERMISSION_READ, $result->getPermissions());
- // Now move the item forcing a new entry in the share table
- \OC\Files\Filesystem::rename($path, 'newPath');
- $this->assertTrue(\OC\Files\Filesystem::file_exists('newPath'));
- $this->assertFalse(\OC\Files\Filesystem::file_exists($path));
+ // Delete the share
+ $this->assertTrue(Filesystem::rmdir($this->folder));
+ $this->assertFalse(Filesystem::file_exists($this->folder));
- // change permissions
- $share->setPermissions($afterPerm);
- $this->shareManager->updateShare($share);
+ // Verify we do not get a share
+ $result = $this->shareManager->getShareById($share->getFullId(), self::TEST_FILES_SHARING_API_USER2);
+ $this->assertEquals(0, $result->getPermissions());
- // Login as user 3 and verify that the permissions are changed
- self::loginHelper(self::TEST_FILES_SHARING_API_USER3);
- $result = $this->shareManager->getShareById($share->getFullId(), self::TEST_FILES_SHARING_API_USER3);
- $this->assertNotEmpty($result);
- $this->assertEquals($afterPerm, $result->getPermissions());
+ // Login as user 1 again and change permissions
+ self::loginHelper(self::TEST_FILES_SHARING_API_USER1);
+ $share->setPermissions(Constants::PERMISSION_ALL);
+ $share = $this->shareManager->updateShare($share);
- // Login as user 2 and verify that the permissions are changed
+ // Login as user 2 and verify
self::loginHelper(self::TEST_FILES_SHARING_API_USER2);
+ $this->assertFalse(Filesystem::file_exists($this->folder));
$result = $this->shareManager->getShareById($share->getFullId(), self::TEST_FILES_SHARING_API_USER2);
- $this->assertNotEmpty($result);
- $this->assertEquals($afterPerm, $result->getPermissions());
- $this->assertEquals('/newPath', $result->getTarget());
+ $this->assertEquals(0, $result->getPermissions());
+
+ $this->shareManager->deleteShare($share);
//cleanup
self::loginHelper(self::TEST_FILES_SHARING_API_USER1);
- $this->shareManager->deleteShare($share);
$testGroup->removeUser($user1);
$testGroup->removeUser($user2);
$testGroup->removeUser($user3);
}
/**
- * If the permissions on a group share are upgraded be sure to still respect
- * removed shares by a member of that group
+ * test if the mount point gets renamed if a folder exists at the target
*/
- public function testPermissionUpgradeOnUserDeletedGroupShare() {
- $testGroup = $this->groupManager->createGroup('testGroup');
- $user1 = $this->userManager->get(self::TEST_FILES_SHARING_API_USER1);
- $user2 = $this->userManager->get(self::TEST_FILES_SHARING_API_USER2);
- $user3 = $this->userManager->get(self::TEST_FILES_SHARING_API_USER3);
- $testGroup->addUser($user1);
- $testGroup->addUser($user2);
- $testGroup->addUser($user3);
+ public function testShareMountOverFolder(): void {
+ self::loginHelper(self::TEST_FILES_SHARING_API_USER2);
+ $this->view2->mkdir('bar');
- $connection = \OC::$server->getDatabaseConnection();
+ self::loginHelper(self::TEST_FILES_SHARING_API_USER1);
- // Share item with group
- $fileinfo = $this->view->getFileInfo($this->folder);
+ // share to user
$share = $this->share(
- \OCP\Share::SHARE_TYPE_GROUP,
+ IShare::TYPE_USER,
$this->folder,
self::TEST_FILES_SHARING_API_USER1,
- 'testGroup',
- \OCP\Constants::PERMISSION_READ
- );
+ self::TEST_FILES_SHARING_API_USER2,
+ Constants::PERMISSION_ALL);
+ $this->shareManager->acceptShare($share, self::TEST_FILES_SHARING_API_USER2);
- // Login as user 2 and verify the item exists
- self::loginHelper(self::TEST_FILES_SHARING_API_USER2);
- $this->assertTrue(\OC\Files\Filesystem::file_exists($this->folder));
- $result = $this->shareManager->getShareById($share->getFullId(), self::TEST_FILES_SHARING_API_USER2);
- $this->assertNotEmpty($result);
- $this->assertEquals(\OCP\Constants::PERMISSION_READ, $result->getPermissions());
+ $share->setTarget('/bar');
+ $this->shareManager->moveShare($share, self::TEST_FILES_SHARING_API_USER2);
- // Delete the share
- $this->assertTrue(\OC\Files\Filesystem::rmdir($this->folder));
- $this->assertFalse(\OC\Files\Filesystem::file_exists($this->folder));
+ $share = $this->shareManager->getShareById($share->getFullId());
- // Verify we do not get a share
- $result = $this->shareManager->getShareById($share->getFullId(), self::TEST_FILES_SHARING_API_USER2);
- $this->assertEquals(0, $result->getPermissions());
+ self::loginHelper(self::TEST_FILES_SHARING_API_USER2);
+ // share should have been moved
- // Login as user 1 again and change permissions
+ $share = $this->shareManager->getShareById($share->getFullId());
+ $this->assertSame('/bar (2)', $share->getTarget());
+
+ //cleanup
self::loginHelper(self::TEST_FILES_SHARING_API_USER1);
- $share->setPermissions(\OCP\Constants::PERMISSION_ALL);
- $share = $this->shareManager->updateShare($share);
+ $this->shareManager->deleteShare($share);
+ $this->view->unlink($this->folder);
+ }
+
+ /**
+ * test if the mount point gets renamed if another share exists at the target
+ */
+ public function testShareMountOverShare(): void {
+ // create a shared cache
+ $caches = [];
+ $cacheFactory = $this->createMock(ICacheFactory::class);
+ $cacheFactory->method('createLocal')
+ ->willReturnCallback(function (string $prefix) use (&$caches) {
+ if (!isset($caches[$prefix])) {
+ $caches[$prefix] = new ArrayCache($prefix);
+ }
+ return $caches[$prefix];
+ });
+ $cacheFactory->method('createDistributed')
+ ->willReturnCallback(function (string $prefix) use (&$caches) {
+ if (!isset($caches[$prefix])) {
+ $caches[$prefix] = new ArrayCache($prefix);
+ }
+ return $caches[$prefix];
+ });
+
+ // hack to overwrite the cache factory, we can't use the proper "overwriteService" since the mount provider is created before this test is called
+ $mountProvider = Server::get(MountProvider::class);
+ $reflectionClass = new \ReflectionClass($mountProvider);
+ $reflectionCacheFactory = $reflectionClass->getProperty('cacheFactory');
+ $reflectionCacheFactory->setAccessible(true);
+ $reflectionCacheFactory->setValue($mountProvider, $cacheFactory);
+
+ // share to user
+ $share = $this->share(
+ IShare::TYPE_USER,
+ $this->folder,
+ self::TEST_FILES_SHARING_API_USER1,
+ self::TEST_FILES_SHARING_API_USER2,
+ Constants::PERMISSION_ALL);
+ $this->shareManager->acceptShare($share, self::TEST_FILES_SHARING_API_USER2);
+
+ $share->setTarget('/foobar');
+ $this->shareManager->moveShare($share, self::TEST_FILES_SHARING_API_USER2);
+
+
+ // share to user
+ $share2 = $this->share(
+ IShare::TYPE_USER,
+ $this->folder2,
+ self::TEST_FILES_SHARING_API_USER1,
+ self::TEST_FILES_SHARING_API_USER2,
+ Constants::PERMISSION_ALL);
+ $this->shareManager->acceptShare($share2, self::TEST_FILES_SHARING_API_USER2);
+
+ $share2->setTarget('/foobar');
+ $this->shareManager->moveShare($share2, self::TEST_FILES_SHARING_API_USER2);
- // Login as user 2 and verify
self::loginHelper(self::TEST_FILES_SHARING_API_USER2);
- $this->assertFalse(\OC\Files\Filesystem::file_exists($this->folder));
- $result = $this->shareManager->getShareById($share->getFullId(), self::TEST_FILES_SHARING_API_USER2);
- $this->assertEquals(0, $result->getPermissions());
+ // one of the shares should have been moved
- $this->shareManager->deleteShare($share);
+ $share = $this->shareManager->getShareById($share->getFullId());
+ $share2 = $this->shareManager->getShareById($share2->getFullId());
+
+ // we don't know or care which share got the "(2)" just that one of them did
+ $this->assertNotEquals($share->getTarget(), $share2->getTarget());
+ $this->assertSame('/foobar', min($share->getTarget(), $share2->getTarget()));
+ $this->assertSame('/foobar (2)', max($share->getTarget(), $share2->getTarget()));
//cleanup
self::loginHelper(self::TEST_FILES_SHARING_API_USER1);
- $testGroup->removeUser($user1);
- $testGroup->removeUser($user2);
- $testGroup->removeUser($user3);
+ $this->shareManager->deleteShare($share);
+ $this->view->unlink($this->folder);
}
-
}
-class DummyTestClassSharedMount extends \OCA\Files_Sharing\SharedMount {
- public function __construct($storage, $mountpoint, $arguments = null, $loader = null){
+class DummyTestClassSharedMount extends SharedMount {
+ public function __construct($storage, $mountpoint, $arguments = null, $loader = null) {
// noop
}