Skip to content
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
40 changes: 26 additions & 14 deletions apps/files/lib/Controller/ViewController.php
Original file line number Diff line number Diff line change
Expand Up @@ -82,16 +82,18 @@ protected function getStorageInfo(string $dir = '/') {
*/
#[NoAdminRequired]
#[NoCSRFRequired]
public function showFile(?string $fileid = null): Response {
public function showFile(?string $fileid = null, ?string $opendetails = null, ?string $openfile = null): Response {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What I meant is adding openfile here is a bugfix ✅
Adding opendetails is a feature I do not see the usecase on this endpoint.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah right, it's kinda both you're right!
I'll adjust the backports to only cover the openfile anyway :)

if (!$fileid) {
return new RedirectResponse($this->urlGenerator->linkToRoute('files.view.index'));
}

// This is the entry point from the `/f/{fileid}` URL which is hardcoded in the server.
try {
return $this->redirectToFile((int)$fileid);
return $this->redirectToFile((int)$fileid, $opendetails, $openfile);
} catch (NotFoundException $e) {
return new RedirectResponse($this->urlGenerator->linkToRoute('files.view.index', ['fileNotFound' => true]));
// Keep the fileid even if not found, it will be used
// to detect the file could not be found and warn the user
return new RedirectResponse($this->urlGenerator->linkToRoute('files.view.indexViewFileid', ['fileid' => $fileid, 'view' => 'files']));
}
}

Expand All @@ -100,38 +102,35 @@ public function showFile(?string $fileid = null): Response {
* @param string $dir
* @param string $view
* @param string $fileid
* @param bool $fileNotFound
* @return TemplateResponse|RedirectResponse
*/
#[NoAdminRequired]
#[NoCSRFRequired]
public function indexView($dir = '', $view = '', $fileid = null, $fileNotFound = false) {
return $this->index($dir, $view, $fileid, $fileNotFound);
public function indexView($dir = '', $view = '', $fileid = null) {
return $this->index($dir, $view, $fileid);
}

/**
* @param string $dir
* @param string $view
* @param string $fileid
* @param bool $fileNotFound
* @return TemplateResponse|RedirectResponse
*/
#[NoAdminRequired]
#[NoCSRFRequired]
public function indexViewFileid($dir = '', $view = '', $fileid = null, $fileNotFound = false) {
return $this->index($dir, $view, $fileid, $fileNotFound);
public function indexViewFileid($dir = '', $view = '', $fileid = null) {
return $this->index($dir, $view, $fileid);
}

/**
* @param string $dir
* @param string $view
* @param string $fileid
* @param bool $fileNotFound
* @return TemplateResponse|RedirectResponse
*/
#[NoAdminRequired]
#[NoCSRFRequired]
public function index($dir = '', $view = '', $fileid = null, $fileNotFound = false) {
public function index($dir = '', $view = '', $fileid = null) {
if ($fileid !== null && $view !== 'trashbin') {
try {
return $this->redirectToFileIfInTrashbin((int)$fileid);
Expand Down Expand Up @@ -159,8 +158,6 @@ public function index($dir = '', $view = '', $fileid = null, $fileNotFound = fal
if (count($nodes) === 1 && $relativePath !== $dir && $nodePath !== $dir) {
return $this->redirectToFile((int)$fileid);
}
} else { // fileid does not exist anywhere
$fileNotFound = true;
}
}

Expand Down Expand Up @@ -249,10 +246,12 @@ private function redirectToFileIfInTrashbin($fileId): RedirectResponse {
* Redirects to the file list and highlight the given file id
*
* @param int $fileId file id to show
* @param string|null $openDetails open details parameter
* @param string|null $openFile open file parameter
* @return RedirectResponse redirect response or not found response
* @throws NotFoundException
*/
private function redirectToFile(int $fileId) {
private function redirectToFile(int $fileId, ?string $openDetails = null, ?string $openFile = null): RedirectResponse {
$uid = $this->userSession->getUser()->getUID();
$baseFolder = $this->rootFolder->getUserFolder($uid);
$node = $baseFolder->getFirstNodeById($fileId);
Expand All @@ -274,6 +273,19 @@ private function redirectToFile(int $fileId) {
// open the file by default (opening the viewer)
$params['openfile'] = 'true';
}

// Forward open parameters if any.
// - openfile is true by default
// - opendetails is undefined by default
// - both will be evaluated as truthy
if ($openDetails !== null) {
$params['opendetails'] = $openDetails !== 'false' ? 'true' : 'false';
}

if ($openFile !== null) {
$params['openfile'] = $openFile !== 'false' ? 'true' : 'false';
}

return new RedirectResponse($this->urlGenerator->linkToRoute('files.view.indexViewFileid', $params));
}

Expand Down
14 changes: 9 additions & 5 deletions apps/files/src/components/FilesListVirtual.vue
Original file line number Diff line number Diff line change
Expand Up @@ -220,23 +220,25 @@ export default defineComponent({
},

openFile: {
async handler(openFile) {
handler(openFile) {
if (!openFile || !this.fileId) {
return
}

await this.handleOpenFile(this.fileId)
this.handleOpenFile(this.fileId)
},
immediate: true,
},

openDetails: {
handler() {
handler(openDetails) {
// wait for scrolling and updating the actions to settle
this.$nextTick(() => {
if (this.fileId && this.openDetails) {
this.openSidebarForFile(this.fileId)
if (!openDetails || !this.fileId) {
return
}

this.openSidebarForFile(this.fileId)
})
},
immediate: true,
Expand Down Expand Up @@ -276,7 +278,9 @@ export default defineComponent({
if (node && sidebarAction?.enabled?.([node], this.currentView)) {
logger.debug('Opening sidebar on file ' + node.path, { node })
sidebarAction.exec(node, this.currentView, this.currentFolder.path)
return
}
logger.error(`Failed to open sidebar on file ${fileId}, file isn't cached yet !`, { fileId, node })
},

scrollToFile(fileId: number|null, warn = true) {
Expand Down
16 changes: 13 additions & 3 deletions apps/files/src/views/FilesList.vue
Original file line number Diff line number Diff line change
Expand Up @@ -525,14 +525,24 @@ export default defineComponent({
},
},

mounted() {
this.fetchContent()

async mounted() {
subscribe('files:node:deleted', this.onNodeDeleted)
subscribe('files:node:updated', this.onUpdatedNode)

// reload on settings change
subscribe('files:config:updated', this.fetchContent)

// Finally, fetch the current directory contents
await this.fetchContent()
if (this.fileId) {
// If we have a fileId, let's check if the file exists
const node = this.dirContents.find(node => node.fileid.toString() === this.fileId.toString())
// If the file isn't in the current directory nor if
// the current directory is the file, we show an error
if (!node && this.currentFolder.fileid.toString() !== this.fileId.toString()) {
showError(t('files', 'The file could not be found'))
}
}
},

unmounted() {
Expand Down
142 changes: 114 additions & 28 deletions apps/files/tests/Controller/ViewControllerTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@
namespace OCA\Files\Tests\Controller;

use OC\Files\FilenameValidator;
use OC\Route\Router;
use OC\URLGenerator;
use OCA\Files\Controller\ViewController;
use OCA\Files\Service\UserConfig;
use OCA\Files\Service\ViewConfig;
Expand All @@ -16,66 +18,106 @@
use OCP\AppFramework\Http\RedirectResponse;
use OCP\AppFramework\Http\TemplateResponse;
use OCP\AppFramework\Services\IInitialState;
use OCP\Diagnostics\IEventLogger;
use OCP\EventDispatcher\IEventDispatcher;
use OCP\Files\File;
use OCP\Files\Folder;
use OCP\Files\IRootFolder;
use OCP\Files\Template\ITemplateManager;
use OCP\ICacheFactory;
use OCP\IConfig;
use OCP\IL10N;
use OCP\IRequest;
use OCP\IURLGenerator;
use OCP\IUser;
use OCP\IUserSession;
use PHPUnit\Framework\MockObject\MockObject;
use Psr\Container\ContainerInterface;
use Psr\Log\LoggerInterface;
use Test\TestCase;

/**
* Class ViewControllerTest
*
* @group RoutingWeirdness
*
* @package OCA\Files\Tests\Controller
*/
class ViewControllerTest extends TestCase {
private IRequest&MockObject $request;
private IURLGenerator&MockObject $urlGenerator;
private IL10N&MockObject $l10n;
private ContainerInterface&MockObject $container;
private IAppManager&MockObject $appManager;
private ICacheFactory&MockObject $cacheFactory;
private IConfig&MockObject $config;
private IEventDispatcher $eventDispatcher;
private IUser&MockObject $user;
private IUserSession&MockObject $userSession;
private IAppManager&MockObject $appManager;
private IRootFolder&MockObject $rootFolder;
private IEventLogger&MockObject $eventLogger;
private IInitialState&MockObject $initialState;
private IL10N&MockObject $l10n;
private IRequest&MockObject $request;
private IRootFolder&MockObject $rootFolder;
private ITemplateManager&MockObject $templateManager;
private IURLGenerator $urlGenerator;
private IUser&MockObject $user;
private IUserSession&MockObject $userSession;
private LoggerInterface&MockObject $logger;
private UserConfig&MockObject $userConfig;
private ViewConfig&MockObject $viewConfig;
private Router $router;

private ViewController&MockObject $viewController;

protected function setUp(): void {
parent::setUp();
$this->request = $this->getMockBuilder(IRequest::class)->getMock();
$this->urlGenerator = $this->getMockBuilder(IURLGenerator::class)->getMock();
$this->l10n = $this->getMockBuilder(IL10N::class)->getMock();
$this->config = $this->getMockBuilder(IConfig::class)->getMock();
$this->appManager = $this->createMock(IAppManager::class);
$this->config = $this->createMock(IConfig::class);
$this->eventDispatcher = $this->createMock(IEventDispatcher::class);
$this->userSession = $this->getMockBuilder(IUserSession::class)->getMock();
$this->appManager = $this->getMockBuilder('\OCP\App\IAppManager')->getMock();
$this->initialState = $this->createMock(IInitialState::class);
$this->l10n = $this->createMock(IL10N::class);
$this->request = $this->createMock(IRequest::class);
$this->rootFolder = $this->createMock(IRootFolder::class);
$this->templateManager = $this->createMock(ITemplateManager::class);
$this->userConfig = $this->createMock(UserConfig::class);
$this->userSession = $this->createMock(IUserSession::class);
$this->viewConfig = $this->createMock(ViewConfig::class);

$this->user = $this->getMockBuilder(IUser::class)->getMock();
$this->user->expects($this->any())
->method('getUID')
->willReturn('testuser1');
$this->userSession->expects($this->any())
->method('getUser')
->willReturn($this->user);
$this->rootFolder = $this->getMockBuilder('\OCP\Files\IRootFolder')->getMock();
$this->initialState = $this->createMock(IInitialState::class);
$this->templateManager = $this->createMock(ITemplateManager::class);
$this->userConfig = $this->createMock(UserConfig::class);
$this->viewConfig = $this->createMock(ViewConfig::class);

$filenameValidator = $this->createMock(FilenameValidator::class);
// Make sure we know the app is enabled
$this->appManager->expects($this->any())
->method('cleanAppId')
->willReturnArgument(0);
$this->appManager->expects($this->any())
->method('getAppPath')
->willReturnCallback(fn (string $appid): string => \OC::$SERVERROOT . '/apps/' . $appid);

$this->cacheFactory = $this->createMock(ICacheFactory::class);
$this->logger = $this->createMock(LoggerInterface::class);
$this->eventLogger = $this->createMock(IEventLogger::class);
$this->container = $this->createMock(ContainerInterface::class);
$this->router = new Router(
$this->logger,
$this->request,
$this->config,
$this->eventLogger,
$this->container,
$this->appManager,
);

// Create a real URLGenerator instance to generate URLs
$this->urlGenerator = new URLGenerator(
$this->config,
$this->userSession,
$this->cacheFactory,
$this->request,
$this->router
);

$filenameValidator = $this->createMock(FilenameValidator::class);
$this->viewController = $this->getMockBuilder(ViewController::class)
->setConstructorArgs([
'files',
Expand Down Expand Up @@ -147,10 +189,60 @@ public function testIndexWithRegularBrowser(): void {
$this->assertEquals($expected, $this->viewController->index('MyDir', 'MyView'));
}

public function dataTestShortRedirect(): array {
// openfile is true by default
// opendetails is undefined by default
// both will be evaluated as truthy
return [
[null, null, '/index.php/apps/files/files/123456?openfile=true'],
['', null, '/index.php/apps/files/files/123456?openfile=true'],
[null, '', '/index.php/apps/files/files/123456?openfile=true&opendetails=true'],
['', '', '/index.php/apps/files/files/123456?openfile=true&opendetails=true'],
['false', '', '/index.php/apps/files/files/123456?openfile=false'],
[null, 'false', '/index.php/apps/files/files/123456?openfile=true&opendetails=false'],
['true', 'false', '/index.php/apps/files/files/123456?openfile=true&opendetails=false'],
['false', 'true', '/index.php/apps/files/files/123456?openfile=false&opendetails=true'],
['false', 'false', '/index.php/apps/files/files/123456?openfile=false&opendetails=false'],
];
}

/**
* @dataProvider dataTestShortRedirect
*/
public function testShortRedirect($openfile, $opendetails, $result) {
$this->appManager->expects($this->any())
->method('isEnabledForUser')
->with('files')
->willReturn(true);

$baseFolderFiles = $this->getMockBuilder(Folder::class)->getMock();
$this->rootFolder->expects($this->any())
->method('getUserFolder')
->with('testuser1')
->willReturn($baseFolderFiles);

$parentNode = $this->getMockBuilder(Folder::class)->getMock();
$parentNode->expects($this->once())
->method('getPath')
->willReturn('testuser1/files/Folder');

$node = $this->getMockBuilder(File::class)->getMock();
$node->expects($this->once())
->method('getParent')
->willReturn($parentNode);

$baseFolderFiles->expects($this->any())
->method('getFirstNodeById')
->with(123456)
->willReturn($node);

$response = $this->viewController->showFile(123456, $opendetails, $openfile);
$this->assertStringContainsString($result, $response->getHeaders()['Location']);
}

public function testShowFileRouteWithTrashedFile(): void {
$this->appManager->expects($this->once())
$this->appManager->expects($this->exactly(2))
->method('isEnabledForUser')
->with('files_trashbin')
->willReturn(true);

$parentNode = $this->getMockBuilder(Folder::class)->getMock();
Expand Down Expand Up @@ -189,13 +281,7 @@ public function testShowFileRouteWithTrashedFile(): void {
->with('testuser1/files_trashbin/files/test.d1462861890/sub')
->willReturn('/test.d1462861890/sub');

$this->urlGenerator
->expects($this->once())
->method('linkToRoute')
->with('files.view.indexViewFileid', ['view' => 'trashbin', 'dir' => '/test.d1462861890/sub', 'fileid' => '123'])
->willReturn('/apps/files/trashbin/123?dir=/test.d1462861890/sub');

$expected = new RedirectResponse('/apps/files/trashbin/123?dir=/test.d1462861890/sub');
$expected = new RedirectResponse('/index.php/apps/files/trashbin/123?dir=/test.d1462861890/sub');
$this->assertEquals($expected, $this->viewController->index('', '', '123'));
}
}
Loading
Loading