Skip to content
Open
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
2 changes: 1 addition & 1 deletion appinfo/info.xml
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ Share your tables and views with users and groups within your cloud.
Have a good time and manage whatever you want.

]]></description>
<version>2.0.0-alpha.1</version>
<version>2.0.0-alpha.2</version>
<licence>agpl</licence>
<author mail="florian.steffens@nextcloud.com">Florian Steffens</author>
<namespace>Tables</namespace>
Expand Down
74 changes: 74 additions & 0 deletions cypress/e2e/tables-sharing-link.cy.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
/**
* SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/

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

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

it('Create, access and delete a public link share', () => {
cy.login(localUser)
cy.visit('apps/tables')

// Create a table
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)

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

cy.contains('Public links').should('be.visible')

// Create public link

cy.intercept('POST', '**/apps/tables/api/2/tables/*/share').as('createShare')
cy.get('button').contains('Create public link').click()
cy.get('.sharing-entry-link__create-form button').contains('Create').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')

// Access the public link
cy.clearCookies()
cy.visit(`apps/tables/s/${shareToken}`)

// Verify public view
cy.get('.public-table-view').should('be.visible')
cy.clickOnTableThreeDotMenu('Export as CSV')

// Verify we cannot edit (simple check: no edit controls or just read-only mode)
// PublicView passes :can-edit-rows="false" etc.
// 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')
})
})
})

2 changes: 2 additions & 0 deletions lib/AppInfo/Application.php
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
use OCA\Tables\Listener\WhenTableTransferredAuditLogListener;
use OCA\Tables\Listener\WhenViewDeletedAuditLogListener;
use OCA\Tables\Middleware\PermissionMiddleware;
use OCA\Tables\Middleware\ShareControlMiddleware;
use OCA\Tables\Reference\ContentReferenceProvider;
use OCA\Tables\Reference\ReferenceProvider;
use OCA\Tables\Search\SearchTablesProvider;
Expand Down Expand Up @@ -92,6 +93,7 @@ public function register(IRegistrationContext $context): void {
$context->registerCapability(Capabilities::class);

$context->registerMiddleware(PermissionMiddleware::class);
$context->registerMiddleware(ShareControlMiddleware::class);
}

public function boot(IBootContext $context): void {
Expand Down
1 change: 1 addition & 0 deletions lib/Constants/ShareReceiverType.php
Original file line number Diff line number Diff line change
Expand Up @@ -14,4 +14,5 @@ class ShareReceiverType {
public const USER = 'user';
public const GROUP = 'group';
public const CIRCLE = 'circle';
public const LINK = 'link';
}
35 changes: 35 additions & 0 deletions lib/Controller/ACommonColumnsController.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
<?php

declare(strict_types=1);
/**
* SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/

namespace OCA\Tables\Controller;

use OCA\Tables\Errors\BadRequestError;
use OCA\Tables\Errors\InternalError;
use OCA\Tables\Errors\NotFoundError;
use OCA\Tables\Errors\PermissionError;
use OCA\Tables\Service\ColumnService;

abstract class ACommonColumnsController extends AOCSController {
protected ColumnService $service;

/**
* @throws PermissionError
* @throws NotFoundError
* @throws InternalError
* @throws BadRequestError
*/
protected function getColumnsFromTableOrView(string $nodeType, int $nodeId, ?string $overriddenUserid = null): array {
if ($nodeType === 'table') {
return $this->service->findAllByTable($nodeId, $overriddenUserid);
}
if ($nodeType === 'view') {
return $this->service->findAllByView($nodeId, $overriddenUserid);
}
throw new BadRequestError('Invalid node type provided');
}
}
98 changes: 65 additions & 33 deletions lib/Controller/ApiColumnsController.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,16 @@
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;
use OCA\Tables\Errors\NotFoundError;
use OCA\Tables\Errors\PermissionError;
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 @@ -24,15 +27,16 @@
/**
* @psalm-import-type TablesColumn from ResponseDefinitions
*/
class ApiColumnsController extends AOCSController {
private ColumnService $service;
class ApiColumnsController extends ACommonColumnsController {

public function __construct(
IRequest $request,
LoggerInterface $logger,
ColumnService $service,
IL10N $n,
string $userId) {
string $userId,
protected ShareService $shareService,
) {
parent::__construct($request, $logger, $n, $userId);
$this->service = $service;
}
Expand All @@ -44,38 +48,39 @@ public function __construct(
*
* @param int $nodeId Node ID
* @param 'table'|'view' $nodeType Node type
* @return DataResponse<Http::STATUS_OK, list<TablesColumn>, array{}>|DataResponse<Http::STATUS_FORBIDDEN|Http::STATUS_INTERNAL_SERVER_ERROR|Http::STATUS_NOT_FOUND, array{message: string}, array{}>
* @return DataResponse<Http::STATUS_OK, list<TablesColumn>,
* 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
*/
#[NoAdminRequired]
#[RequirePermission(permission: Application::PERMISSION_READ)]
public function index(int $nodeId, string $nodeType): DataResponse {
try {
if ($nodeType === 'table') {
$columns = $this->service->findAllByTable($nodeId);
} elseif ($nodeType === 'view') {
$columns = $this->service->findAllByView($nodeId);
} else {
$columns = null;
}
$columns = $this->getColumnsFromTableOrView($nodeType, $nodeId);
return new DataResponse($this->service->formatColumns($columns));
} catch (PermissionError $e) {
return $this->handlePermissionError($e);
} catch (InternalError $e) {
return $this->handleError($e);
} catch (NotFoundError $e) {
return $this->handleNotFoundError($e);
} catch (BadRequestError $e) {
return $this->handleBadRequestError($e);
}
}

/**
* [api v2] Get a column object
*
* @param int $id Column ID
* @return DataResponse<Http::STATUS_OK, TablesColumn, array{}>|DataResponse<Http::STATUS_FORBIDDEN|Http::STATUS_INTERNAL_SERVER_ERROR|Http::STATUS_NOT_FOUND, array{message: string}, array{}>
* @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 returned
* 403: No permissions
Expand Down Expand Up @@ -111,10 +116,14 @@ public function show(int $id): DataResponse {
* @param float|null $numberMax Max
* @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 array<string, mixed> $customSettings Custom settings for the column
* @param list<int>|null $selectedViewIds View IDs where this columns
* should be added
* @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{}>
* @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
Expand Down Expand Up @@ -161,14 +170,19 @@ public function createNumberColumn(int $baseNodeId, string $title, ?float $numbe
* @param string|null $textDefault Default
* @param string|null $textAllowedPattern Allowed regex pattern
* @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 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 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{}>
* @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
Expand Down Expand Up @@ -210,16 +224,23 @@ public function createTextColumn(int $baseNodeId, string $title, ?string $textDe
*
* @param int $baseNodeId Context of the column creation
* @param string $title Title
* @param string $selectionOptions Json array{id: int, label: string} with options that can be selected, eg [{"id": 1, "label": "first"},{"id": 2, "label": "second"}]
* @param string|null $selectionDefault Json int|list<int> for default selected option(s), eg 5 or ["1", "8"]
* @param string $selectionOptions Json array{id: int, label: string} with
* options that can be selected, eg [{"id": 1, "label": "first"},{"id":
* 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 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
* @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{}>
* @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
Expand Down Expand Up @@ -259,15 +280,20 @@ public function createSelectionColumn(int $baseNodeId, string $title, string $se
*
* @param int $baseNodeId Context of the column creation
* @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 '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 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
* @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{}>
* @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
Expand Down Expand Up @@ -304,19 +330,25 @@ public function createDatetimeColumn(int $baseNodeId, string $title, ?string $da
*
* @param int $baseNodeId Context of the column creation
* @param string $title Title
* @param string|null $usergroupDefault Json array{id: string, type: int}, eg [{"id": "admin", "type": 0}, {"id": "user1", "type": 0}]
* @param boolean $usergroupMultipleItems Whether you can select multiple users or/and groups
* @param string|null $usergroupDefault Json array{id: string, type: int},
* 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 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
* @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{}>
* @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
Expand Down
Loading
Loading