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
64 changes: 64 additions & 0 deletions css/error.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
/**
* SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/

.tables-error-wrapper {
display: flex;
justify-content: center;
align-items: center;
width: 100%;
}

.body-public-container {
--color-text-maxcontrast: var(
--color-text-maxcontrast-background-blur,
var(--color-main-text)
);
color: var(--color-main-text);
background-color: var(--color-main-background-blur);
padding: calc(3 * var(--default-grid-baseline));
box-shadow: 0 0 10px var(--color-box-shadow);
-webkit-backdrop-filter: var(--filter-background-blur);
backdrop-filter: var(--filter-background-blur);
display: flex;
flex-direction: column;
text-align: start;
word-wrap: break-word;
border-radius: 10px;
cursor: default;
-moz-user-select: text;
-webkit-user-select: text;
-ms-user-select: text;
user-select: text;
height: fit-content;
width: 100%;
max-width: 700px;
margin-block: 10vh auto;
}

.body-public-container .icon-big {
background-size: 70px;
height: 70px;
}

.body-public-container form {
width: initial;
}

.body-public-container p:not(:last-child) {
margin-bottom: 12px;
}

.infogroup {
margin: 8px 0;
}

.infogroup:last-child {
margin-bottom: 0;
}

.update {
width: calc(100% - 32px);
text-align: center;
}
121 changes: 121 additions & 0 deletions cypress/e2e/tables-sharing-link.cy.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
/**
* SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/

describe('Public link sharing', () => {
let localUser
const tableTitle = 'Public Share Test Table'

before(() => {
cy.createRandomUser().then(user => {
localUser = user
cy.login(localUser)
})
})

beforeEach(() => {
cy.login(localUser)
cy.visit('apps/tables')
cy.get('[data-cy="navigationCreateTableIcon"]').click({ force: true })
cy.get('.modal__content input[type="text"]').clear().type(tableTitle)
cy.get('.tile').contains('ToDo').click({ force: true })
cy.get('[data-cy="createTableSubmitBtn"]').scrollIntoView().click()
cy.loadTable(tableTitle)
})

it('Create, access and delete a public link share', () => {

cy.get('[data-cy="customTableAction"] button').click()
cy.get('[data-cy="dataTableShareBtn"]').click()
cy.contains('Public links').should('be.visible')
cy.intercept('POST', '**/apps/tables/api/2/tables/*/share').as('createShare')
cy.get('[data-cy="sharingEntryLinkCreateButton"]').click()
cy.get('[data-cy="sharingEntryLinkCreateFormCreateButton"]').click()

cy.wait('@createShare').then((interception) => {
expect(interception.response.statusCode).to.eq(200)
const shareToken = interception.response.body.ocs.data.shareToken
expect(shareToken).to.be.a('string')

cy.clearCookies()
cy.visit(`apps/tables/s/${shareToken}`)
cy.get('[data-cy="publicTableElement"]').should('be.visible')
cy.clickOnTableThreeDotMenu('Export as CSV')

// Verify we cannot edit (simple check: no edit controls or just read-only mode)
// We can check that the "Add row" button is missing.
cy.get('[data-cy="addRowBtn"]').should('not.exist')

// Login again to delete share
cy.login(localUser)
cy.visit('apps/tables')
cy.loadTable(tableTitle)

cy.get('[data-cy="customTableAction"] button').click()
cy.get('[data-cy="dataTableShareBtn"]').click()

cy.get('[data-cy="sharingEntryLinkTitle"]').should('be.visible')
cy.get('[data-cy="sharingEntryLinkDeleteButton"]').click()

cy.get('[data-cy="sharingEntryLinkTitle"]').should('not.exist')

// Verify share is gone
cy.clearCookies()
cy.visit(`apps/tables/s/${shareToken}`, { failOnStatusCode: false })
cy.get('h2').contains('Share not found').should('be.visible')
})
})

it('Create, access and delete a password protected public link share', () => {
const password = 'extremelySafePassword123'

cy.get('[data-cy="customTableAction"] button').click()
cy.get('[data-cy="dataTableShareBtn"]').click()
cy.contains('Public links').should('be.visible')
cy.intercept('POST', '**/apps/tables/api/2/tables/*/share').as('createShare')

// Open create form
cy.get('[data-cy="sharingEntryLinkCreateButton"]').click()

// Set password
cy.get('[data-cy="sharingEntryLinkPasswordCheck"]').click()
cy.get('[data-cy="sharingEntryLinkPasswordInput"] input').type(password)

// Create
cy.get('[data-cy="sharingEntryLinkCreateFormCreateButton"]').click()

cy.wait('@createShare').then((interception) => {
expect(interception.response.statusCode).to.eq(200)
const shareToken = interception.response.body.ocs.data.shareToken
expect(shareToken).to.be.a('string')

cy.clearCookies()
cy.visit(`apps/tables/s/${shareToken}`)

// Password Gate
cy.get('input[type="password"]#password').should('be.visible').type(password)
cy.get('input#password-submit').click()
cy.get('[data-cy="publicTableElement"]').should('be.visible')

// Login again to delete share
cy.login(localUser)
cy.visit('apps/tables')
cy.loadTable(tableTitle)

cy.get('[data-cy="customTableAction"] button').click()
cy.get('[data-cy="dataTableShareBtn"]').click()

cy.get('[data-cy="sharingEntryLinkTitle"]').should('be.visible')
cy.get('[data-cy="sharingEntryLinkDeleteButton"]').click()

cy.get('[data-cy="sharingEntryLinkTitle"]').should('not.exist')

// Verify share is gone
cy.clearCookies()
cy.visit(`apps/tables/s/${shareToken}`, { failOnStatusCode: false })
cy.get('h2').contains('Share not found').should('be.visible')
})
})
})

26 changes: 26 additions & 0 deletions lib/Controller/ApiColumnsController.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,11 @@
* SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/

Copy link
Member

Choose a reason for hiding this comment

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

I think all the changes in this file are not necessary?

namespace OCA\Tables\Controller;

use OCA\Tables\AppInfo\Application;
use OCA\Tables\Db\Column;
use OCA\Tables\Dto\Column as ColumnDto;
use OCA\Tables\Errors\BadRequestError;
use OCA\Tables\Errors\InternalError;
Expand All @@ -15,6 +17,7 @@
use OCA\Tables\Middleware\Attribute\RequirePermission;
use OCA\Tables\ResponseDefinitions;
use OCA\Tables\Service\ColumnService;
use OCA\Tables\Service\ShareService;
use OCP\AppFramework\Http;
use OCP\AppFramework\Http\Attribute\NoAdminRequired;
use OCP\AppFramework\Http\DataResponse;
Expand All @@ -27,12 +30,14 @@
*/
class ApiColumnsController extends ACommonColumnsController {


public function __construct(
IRequest $request,
LoggerInterface $logger,
ColumnService $service,
IL10N $n,
string $userId,
protected ShareService $shareService,
) {
parent::__construct($request, $logger, $n, $userId);
$this->service = $service;
Expand All @@ -49,8 +54,10 @@ public function __construct(
* array{}>|DataResponse<Http::STATUS_BAD_REQUEST|Http::STATUS_FORBIDDEN|Http::STATUS_INTERNAL_SERVER_ERROR|Http::STATUS_NOT_FOUND,
* array{message: string}, array{}>
*
*
* 200: View deleted
* 400: Invalid input arguments
*
* 403: No permissions
* 404: Not found
*/
Expand All @@ -59,6 +66,7 @@ public function __construct(
public function index(int $nodeId, string $nodeType): DataResponse {
try {
$columns = $this->getColumnsFromTableOrView($nodeType, $nodeId);

return new DataResponse($this->service->formatColumns($columns));
} catch (PermissionError $e) {
return $this->handlePermissionError($e);
Expand All @@ -79,6 +87,7 @@ public function index(int $nodeId, string $nodeType): DataResponse {
* array{}>|DataResponse<Http::STATUS_FORBIDDEN|Http::STATUS_INTERNAL_SERVER_ERROR|Http::STATUS_NOT_FOUND,
* array{message: string}, array{}>
*
*
* 200: Column returned
* 403: No permissions
* 404: Not found
Expand Down Expand Up @@ -118,10 +127,12 @@ public function show(int $id): DataResponse {
* @param array<string, mixed> $customSettings Custom settings for the
* column
*
*
* @return DataResponse<Http::STATUS_OK, TablesColumn,
* array{}>|DataResponse<Http::STATUS_FORBIDDEN|Http::STATUS_INTERNAL_SERVER_ERROR|Http::STATUS_NOT_FOUND,
* array{message: string}, array{}>
*
*
* 200: Column created
* 403: No permission
* 404: Not found
Expand Down Expand Up @@ -169,10 +180,12 @@ public function createNumberColumn(int $baseNodeId, string $title, ?float $numbe
* @param int|null $textMaxLength Max raw text length
* @param bool|null $textUnique Whether the text value must be unique, if
* column is a text
*
* @param 'progress'|'stars'|null $subtype Subtype for the new column
* @param string|null $description Description
* @param list<int>|null $selectedViewIds View IDs where this columns
* should be added
*
* @param boolean $mandatory Is mandatory
* @param 'table'|'view' $baseNodeType Context type of the column creation
* @param array<string, mixed> $customSettings Custom settings for the
Expand All @@ -181,6 +194,7 @@ public function createNumberColumn(int $baseNodeId, string $title, ?float $numbe
* array{}>|DataResponse<Http::STATUS_FORBIDDEN|Http::STATUS_INTERNAL_SERVER_ERROR|Http::STATUS_NOT_FOUND,
* array{message: string}, array{}>
*
*
* 200: Column created
* 403: No permission
* 404: Not found
Expand Down Expand Up @@ -226,19 +240,23 @@ public function createTextColumn(int $baseNodeId, string $title, ?string $textDe
* 2, "label": "second"}]
* @param string|null $selectionDefault Json int|list<int> for default
* selected option(s), eg 5 or ["1", "8"]
*
* @param 'progress'|'stars'|null $subtype Subtype for the new column
* @param string|null $description Description
* @param list<int>|null $selectedViewIds View IDs where this columns
* should be added
*
* @param boolean $mandatory Is mandatory
* @param 'table'|'view' $baseNodeType Context type of the column creation
* @param array<string, mixed> $customSettings Custom settings for the
* column
*
*
* @return DataResponse<Http::STATUS_OK, TablesColumn,
* array{}>|DataResponse<Http::STATUS_FORBIDDEN|Http::STATUS_INTERNAL_SERVER_ERROR|Http::STATUS_NOT_FOUND,
* array{message: string}, array{}>
*
*
* 200: Column created
* 403: No permission
* 404: Not found
Expand Down Expand Up @@ -279,19 +297,23 @@ public function createSelectionColumn(int $baseNodeId, string $title, string $se
* @param string $title Title
* @param 'today'|'now'|null $datetimeDefault For a subtype 'date' you can
* set 'today'. For a main type or subtype 'time' you can set to 'now'.
*
* @param 'progress'|'stars'|null $subtype Subtype for the new column
* @param string|null $description Description
* @param list<int>|null $selectedViewIds View IDs where this columns
* should be added
*
* @param boolean $mandatory Is mandatory
* @param 'table'|'view' $baseNodeType Context type of the column creation
* @param array<string, mixed> $customSettings Custom settings for the
* column
*
*
* @return DataResponse<Http::STATUS_OK, TablesColumn,
* array{}>|DataResponse<Http::STATUS_FORBIDDEN|Http::STATUS_INTERNAL_SERVER_ERROR|Http::STATUS_NOT_FOUND,
* array{message: string}, array{}>
*
*
* 200: Column created
* 403: No permission
* 404: Not found
Expand Down Expand Up @@ -331,22 +353,26 @@ public function createDatetimeColumn(int $baseNodeId, string $title, ?string $da
* eg [{"id": "admin", "type": 0}, {"id": "user1", "type": 0}]
* @param boolean $usergroupMultipleItems Whether you can select multiple
* users or/and groups
*
* @param boolean $usergroupSelectUsers Whether you can select users
* @param boolean $usergroupSelectGroups Whether you can select groups
* @param boolean $usergroupSelectTeams Whether you can select teams
* @param boolean $showUserStatus Whether to show the user's status
* @param string|null $description Description
* @param list<int>|null $selectedViewIds View IDs where this columns
* should be added
*
* @param boolean $mandatory Is mandatory
* @param 'table'|'view' $baseNodeType Context type of the column creation
* @param array<string, mixed> $customSettings Custom settings for the
* column
*
*
* @return DataResponse<Http::STATUS_OK, TablesColumn,
* array{}>|DataResponse<Http::STATUS_FORBIDDEN|Http::STATUS_INTERNAL_SERVER_ERROR|Http::STATUS_NOT_FOUND,
* array{message: string}, array{}>
*
*
* 200: Column created
* 403: No permission
* 404: Not found
Expand Down
4 changes: 3 additions & 1 deletion lib/Controller/ApiPublicColumnsController.php
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,9 @@ public function indexByPublicLink(string $token): DataResponse {
$shareToken = new ShareToken($token);
} catch (InvalidArgumentException $e) {
return $this->handleBadRequestError(new BadRequestError(
'Invalid share token', $e->getCode(), $e
'Invalid share token',
$e->getCode(),
$e
));
}

Expand Down
2 changes: 1 addition & 1 deletion lib/Controller/PageController.php
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,6 @@ protected function loadStyles(): void {
Util::addStyle(Application::APP_ID, 'grid');
Util::addStyle(Application::APP_ID, 'modal');
Util::addStyle(Application::APP_ID, 'tiptap');
Util::addStyle(Application::APP_ID, 'tables-style');
Util::addStyle(Application::APP_ID, 'error');
}
}
Loading
Loading