diff --git a/cypress/e2e/propfind.spec.js b/cypress/e2e/propfind.spec.js new file mode 100644 index 00000000000..d656962e3ac --- /dev/null +++ b/cypress/e2e/propfind.spec.js @@ -0,0 +1,89 @@ +/** + * @copyright Copyright (c) 2022 Max + * + * @author Max + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * 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 + * along with this program. If not, see . + * + */ + +import { randHash } from '../utils/index.js' +const randUser = randHash() + +describe('Text PROPFIND extension ', function() { + const richWorkspace = '{http://nextcloud.org/ns}rich-workspace' + + before(function() { + cy.nextcloudCreateUser(randUser, 'password') + }) + + beforeEach(function() { + cy.login(randUser, 'password') + }) + + describe('with workspaces enabled', function() { + + beforeEach(function() { + cy.configureText('workspace_enabled', 1) + }) + + // Android app relies on this to detect rich workspace availability + it('always adds rich workspace property', function() { + cy.uploadFile('empty.md', 'text/markdown', '/Readme.md') + cy.propfindFolder('/') + .should('have.property', richWorkspace, '') + cy.uploadFile('test.md', 'text/markdown', '/Readme.md') + cy.propfindFolder('/') + .should('have.property', richWorkspace, '## Hello world\n') + cy.removeFile('/Readme.md') + cy.propfindFolder('/') + .should('have.property', richWorkspace, '') + }) + + // Android app relies on this when navigating nested folders + it('adds rich workspace property to nested folders', function() { + cy.createFolder('/workspace') + cy.propfindFolder('/', 1) + .then(results => results.pop().propStat[0].properties) + .should('have.property', richWorkspace, '') + cy.uploadFile('test.md', 'text/markdown', '/workspace/Readme.md') + cy.propfindFolder('/', 1) + .then(results => results.pop().propStat[0].properties) + .should('have.property', richWorkspace, '## Hello world\n') + }) + + }) + + describe('with workspaces disabled', function() { + + beforeEach(function() { + cy.configureText('workspace_enabled', 0) + }) + + it('does not return a rich workspace property', function() { + cy.propfindFolder('/') + .should('not.have.property', richWorkspace) + cy.uploadFile('test.md', 'text/markdown', '/Readme.md') + cy.propfindFolder('/') + .should('not.have.property', richWorkspace) + cy.createFolder('/without-workspace') + cy.propfindFolder('/', 1) + .then(results => results.pop().propStat[0].properties) + .should('not.have.property', richWorkspace) + }) + + }) +}) diff --git a/cypress/support/commands.js b/cypress/support/commands.js index 4b3a5e94c64..64b98c27d39 100644 --- a/cypress/support/commands.js +++ b/cypress/support/commands.js @@ -108,10 +108,10 @@ Cypress.Commands.add('uploadFile', (fileName, mimeType, target) => { if (typeof target !== 'undefined') { fileName = target } - await cy.window().then(async window => { + await cy.window().then(async win => { await axios.put(`${Cypress.env('baseUrl')}/remote.php/webdav/${fileName}`, file, { headers: { - requesttoken: window.OC.requestToken, + requesttoken: win.OC.requestToken, 'Content-Type': mimeType, }, }).then(response => { @@ -128,10 +128,10 @@ Cypress.Commands.add('createFile', (target, content, mimeType) => { const file = new File([blob], fileName, { type: mimeType }) return cy.window() - .then(async window => { + .then(async win => { const response = await axios.put(`${Cypress.env('baseUrl')}/remote.php/webdav/${target}`, file, { headers: { - requesttoken: window.OC.requestToken, + requesttoken: win.OC.requestToken, 'Content-Type': mimeType, }, }) @@ -162,30 +162,52 @@ Cypress.Commands.add('shareFileToUser', (userId, password, path, targetUserId) = }) }) -Cypress.Commands.add('createFolder', dirName => { - return cy.window().then(win => { - return win.OC.Files.getClient().createDirectory(dirName) - }) -}) +Cypress.Commands.add('createFolder', dirName => cy.window() + .then(win => win.OC.Files.getClient().createDirectory(dirName)) +) -Cypress.Commands.add('moveFile', (path, destinationPath) => { - return cy.window().then(win => { - return win.OC.Files.getClient().move(path, destinationPath) - }) -}) +Cypress.Commands.add('moveFile', (path, destinationPath) => cy.window() + .then(win => win.OC.Files.getClient().move(path, destinationPath)) +) -Cypress.Commands.add('copyFile', (path, destinationPath) => { - return cy.window().then(win => { - return win.OC.Files.getClient().copy(path, destinationPath) - }) -}) +Cypress.Commands.add('removeFile', (path) => cy.window() + .then(win => win.OC.Files.getClient().remove(path)) +) -Cypress.Commands.add('reloadFileList', () => { - return cy.window().then(win => { - return win.OCA?.Files?.App?.fileList?.reload() - }) +Cypress.Commands.add('copyFile', (path, destinationPath) => cy.window() + .then(win => win.OC.Files.getClient().copy(path, destinationPath)) +) + +Cypress.Commands.add('propfindFolder', (path, depth = 0) => { + return cy.window() + .then(win => { + const files = win.OC.Files + const PROPERTY_WORKSPACE_FILE + = `{${files.Client.NS_NEXTCLOUD}}rich-workspace-file` + const PROPERTY_WORKSPACE + = `{${files.Client.NS_NEXTCLOUD}}rich-workspace` + const properties = [ + ...files.getClient().getPropfindProperties(), + PROPERTY_WORKSPACE_FILE, + PROPERTY_WORKSPACE, + ] + const client = files.getClient().getClient() + return client.propFind(client.baseUrl + path, properties, depth) + .then((results) => { + cy.log(`Propfind returned ${results.status}`) + if (depth) { + return results.body + } else { + return results.body.propStat[0].properties + } + }) + }) }) +Cypress.Commands.add('reloadFileList', () => cy.window() + .then(win => win.OCA?.Files?.App?.fileList?.reload()) +) + Cypress.Commands.add('openFile', (fileName, params = {}) => { cy.get(`#fileList tr[data-file="${fileName}"] a.name`).click(params) // eslint-disable-next-line cypress/no-unnecessary-waiting @@ -231,3 +253,13 @@ Cypress.Commands.add('openWorkspace', (subject, name) => { return cy.getContent() }) + +Cypress.Commands.add('configureText', (key, value) => { + return cy.window().then(win => { + return axios.post( + `${Cypress.env('baseUrl')}/index.php/apps/text/settings`, + { key, value }, + { headers: { requesttoken: win.OC.requestToken } } + ) + }) +}) diff --git a/lib/DAV/WorkspacePlugin.php b/lib/DAV/WorkspacePlugin.php index 2f88f2d8d8d..2967833f688 100644 --- a/lib/DAV/WorkspacePlugin.php +++ b/lib/DAV/WorkspacePlugin.php @@ -84,6 +84,11 @@ public function initialize(Server $server) { public function propFind(PropFind $propFind, INode $node) { + if (!in_array(self::WORKSPACE_PROPERTY, $propFind->getRequestedProperties()) + && !in_array(self::WORKSPACE_FILE_PROPERTY, $propFind->getRequestedProperties())) { + return; + } + if (!$node instanceof Directory && !$node instanceof FilesHome) { return; } @@ -95,27 +100,31 @@ public function propFind(PropFind $propFind, INode $node) { return; } - // Only return the property for the parent node and ignore it for further in depth nodes - if ($propFind->getDepth() === $this->server->getHTTPDepth()) { - $owner = $this->userId ?? $node->getFileInfo()->getStorage()->getOwner(''); - /** @var Folder[] $nodes */ - $nodes = $this->rootFolder->getUserFolder($owner)->getById($node->getId()); - if (count($nodes) > 0) { - try { - /** @var File $file */ - $file = $this->workspaceService->getFile($nodes[0]); - if ($file instanceof File) { - $propFind->handle(self::WORKSPACE_PROPERTY, function () use ($file) { - return $file->getContent(); - }); - $propFind->handle(self::WORKSPACE_FILE_PROPERTY, function () use ($file) { - return $file->getFileInfo()->getId(); - }); - } - } catch (StorageNotAvailableException $e) { - // If a storage is not available we can for the propfind response assume that there is no rich workspace present - } + $file = null; + $owner = $this->userId ?? $node->getFileInfo()->getStorage()->getOwner(''); + /** @var Folder[] $nodes */ + $nodes = $this->rootFolder->getUserFolder($owner)->getById($node->getId()); + if (count($nodes) > 0) { + /** @var File $file */ + try { + $file = $this->workspaceService->getFile($nodes[0]); + } catch (StorageNotAvailableException $e) { + // If a storage is not available we can for the propfind response assume that there is no rich workspace present } } + + // Only return the property for the parent node and ignore it for further in depth nodes + $propFind->handle(self::WORKSPACE_PROPERTY, function () use ($file) { + if ($file instanceof File) { + return $file->getContent(); + } + return ''; + }); + $propFind->handle(self::WORKSPACE_FILE_PROPERTY, function () use ($file) { + if ($file instanceof File) { + return $file->getFileInfo()->getId(); + } + return ''; + }); } }