Skip to content

Commit c331e92

Browse files
committed
enh(Sharing): backend infrastructre for read-only link shares
- modifies oc_tables_share structure with two columns, token and password - adds ShareOCSController with a route to create link shares - adds a ShareToken value object - extends Share entity with ShareToken and Password properties - extends ShareMapper to find a share by the share token - extends ShareService with a method to easily create link shares Signed-off-by: Arthur Schiwon <blizzz@arthur-schiwon.de>
1 parent eee8bd7 commit c331e92

9 files changed

Lines changed: 269 additions & 33 deletions

File tree

lib/Constants/ShareReceiverType.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,4 +14,5 @@ class ShareReceiverType {
1414
public const USER = 'user';
1515
public const GROUP = 'group';
1616
public const CIRCLE = 'circle';
17+
public const LINK = 'link';
1718
}
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
/**
5+
* SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
6+
* SPDX-License-Identifier: AGPL-3.0-or-later
7+
*/
8+
9+
namespace OCA\Tables\Controller;
10+
11+
use OCA\Tables\AppInfo\Application;
12+
use OCA\Tables\Errors\BadRequestError;
13+
use OCA\Tables\Helper\ConversionHelper;
14+
use OCA\Tables\Middleware\Attribute\RequirePermission;
15+
use OCA\Tables\Service\ShareService;
16+
use OCA\Tables\Service\TableService;
17+
use OCA\Tables\Service\ViewService;
18+
use OCP\AppFramework\Http\Attribute\ApiRoute;
19+
use OCP\AppFramework\Http\Attribute\NoAdminRequired;
20+
use OCP\AppFramework\Http\DataResponse;
21+
use OCP\EventDispatcher\IEventDispatcher;
22+
use OCP\IL10N;
23+
use OCP\IRequest;
24+
use OCP\IURLGenerator;
25+
use OCP\Security\Events\ValidatePasswordPolicyEvent;
26+
use OCP\Security\PasswordContext;
27+
use Psr\Log\LoggerInterface;
28+
29+
class ShareOCSController extends AOCSController {
30+
public function __construct(
31+
IRequest $request,
32+
LoggerInterface $logger,
33+
IL10N $n,
34+
string $userId,
35+
protected ShareService $shareService,
36+
protected TableService $tableService,
37+
protected ViewService $viewService,
38+
protected IEventDispatcher $eventDispatcher,
39+
protected IURLGenerator $urlGenerator,
40+
) {
41+
parent::__construct($request, $logger, $n, $userId);
42+
}
43+
44+
#[NoAdminRequired]
45+
#[RequirePermission(permission: Application::PERMISSION_MANAGE, typeParam: 'nodeCollection')]
46+
#[ApiRoute(verb: 'POST', url: '/api/2/{nodeCollection}/{nodeId}/share')]
47+
public function createLinkShare(
48+
string $nodeCollection,
49+
string $nodeId,
50+
?string $password = null,
51+
): DataResponse {
52+
$collection = ConversionHelper::stringNodeType2Const($nodeCollection);
53+
if ($collection === Application::NODE_TYPE_TABLE) {
54+
$node = $this->tableService->find($nodeId);
55+
} else {
56+
$node = $this->viewService->find($nodeId);
57+
}
58+
59+
if ($password !== null) {
60+
$event = new ValidatePasswordPolicyEvent($password, PasswordContext::SHARING);
61+
try {
62+
$this->eventDispatcher->dispatchTyped($event);
63+
} catch (\Exception $e) {
64+
$error = new BadRequestError($e->getMessage(), $e->getCode(), $e);
65+
return $this->handleBadRequestError($error);
66+
}
67+
}
68+
69+
$share = $this->shareService->createLinkShare($node, $password);
70+
return new DataResponse([
71+
'shareToken' => (string)$share->getToken(),
72+
'url' => $this->urlGenerator->linkToRouteAbsolute('tables.page.public', ['shareToken' => (string)$share->getToken()]),
73+
]);
74+
}
75+
}

lib/Db/Share.php

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
use JsonSerializable;
1111

1212
use OCA\Tables\ResponseDefinitions;
13+
use OCA\Tables\Service\ValueObject\ShareToken;
1314

1415
/**
1516
* @psalm-import-type TablesShare from ResponseDefinitions
@@ -28,6 +29,9 @@
2829
* @method setNodeId(int $nodeId)
2930
* @method getNodeType(): string
3031
* @method setNodeType(string $nodeType)
32+
* @method getToken(): ShareToken
33+
* @method getPassword(): string
34+
* @method setPassword(string $password)
3135
* @method getPermissionRead(): bool
3236
* @method setPermissionRead(bool $permissionRead)
3337
* @method getPermissionCreate(): bool
@@ -50,6 +54,8 @@ class Share extends EntitySuper implements JsonSerializable {
5054
protected ?string $receiverType = null; // user, group, circle
5155
protected ?int $nodeId = null;
5256
protected ?string $nodeType = null;
57+
protected ?ShareToken $token = null;
58+
protected ?string $password = null;
5359
protected ?bool $permissionRead = null;
5460
protected ?bool $permissionCreate = null;
5561
protected ?bool $permissionUpdate = null;
@@ -75,6 +81,10 @@ public function __construct() {
7581
$this->addType('permissionManage', 'boolean');
7682
}
7783

84+
public function setToken(string $token): void {
85+
$this->token = new ShareToken($token);
86+
}
87+
7888
/**
7989
* @psalm-return TablesShare
8090
*/
@@ -92,6 +102,8 @@ public function jsonSerialize(): array {
92102
'receiver' => $this->receiver,
93103
'receiverDisplayName' => $this->receiverDisplayName,
94104
'receiverType' => $this->receiverType,
105+
'token' => $this->token,
106+
'password' => $this->password,
95107
'createdAt' => $this->createdAt,
96108
'lastEditAt' => $this->lastEditAt,
97109
];

lib/Db/ShareMapper.php

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77

88
namespace OCA\Tables\Db;
99

10+
use OCA\Tables\Service\ValueObject\ShareToken;
1011
use OCP\AppFramework\Db\DoesNotExistException;
1112
use OCP\AppFramework\Db\MultipleObjectsReturnedException;
1213
use OCP\AppFramework\Db\QBMapper;
@@ -41,6 +42,16 @@ public function find(int $id): Share {
4142
return $this->findEntity($qb);
4243
}
4344

45+
public function findByToken(ShareToken $token): Share {
46+
$qb = $this->db->getQueryBuilder();
47+
$qb->select('*')
48+
->from($this->table)
49+
->where($qb->expr()->eq('token', $qb->createNamedParameter((string)$token, IQueryBuilder::PARAM_STR)));
50+
return $this->findEntity($qb);
51+
}
52+
53+
public function createLinkShare()
54+
4455
/**
4556
* find share for a node
4657
* look for all receiver types or limit it to one given type

lib/Helper/ConversionHelper.php

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@
99

1010
use InvalidArgumentException;
1111
use OCA\Tables\AppInfo\Application;
12+
use OCA\Tables\Db\Table;
13+
use OCA\Tables\Db\View;
1214

1315
class ConversionHelper {
1416

@@ -33,4 +35,11 @@ public static function stringNodeType2Const(string $nodeType): int {
3335
default => throw new InvalidArgumentException('Invalid node type'),
3436
};
3537
}
38+
39+
public static function object2String(Table|View $node): string {
40+
if ($node instanceof Table) {
41+
return 'table';
42+
}
43+
return 'view';
44+
}
3645
}

lib/Migration/Version000200Date20220428000000.php

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,14 @@ public function changeSchema(IOutput $output, Closure $schemaClosure, array $opt
5454
'notnull' => true,
5555
'length' => 50
5656
]);
57+
$table->addColumn('token', Types::STRING, [
58+
'notnull' => false,
59+
'length' => 64
60+
]);
61+
$table->addColumn('password', Types::STRING, [
62+
'notnull' => false,
63+
'length' => 255
64+
]);
5765

5866
$table->addColumn('permission_read', Types::BOOLEAN, [
5967
'notnull' => false,
@@ -83,6 +91,8 @@ public function changeSchema(IOutput $output, Closure $schemaClosure, array $opt
8391
'notnull' => true,
8492
]);
8593
$table->setPrimaryKey(['id']);
94+
$table->addIndex(['node_id', 'node_type'], 'shares_node_idx');
95+
$table->addIndex(['receiver', 'receiver_type'], 'shares_receiver_idx');
8696
}
8797

8898
return $schema;
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
/**
6+
* SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
7+
* SPDX-License-Identifier: AGPL-3.0-or-later
8+
*/
9+
10+
namespace OCA\Tables\Migration;
11+
12+
use Closure;
13+
use OCP\DB\ISchemaWrapper;
14+
use OCP\DB\Types;
15+
use OCP\Migration\IOutput;
16+
use OCP\Migration\SimpleMigrationStep;
17+
use Override;
18+
19+
class Version1000Date20251208192653 extends SimpleMigrationStep {
20+
21+
#[Override]
22+
public function changeSchema(IOutput $output, Closure $schemaClosure, array $options): ?ISchemaWrapper {
23+
/** @var ISchemaWrapper $schema */
24+
$schema = $schemaClosure();
25+
$tableName = 'tables_shares';
26+
if (!$schema->hasTable($tableName)) {
27+
return null;
28+
}
29+
30+
$table = $schema->getTable($tableName);
31+
if (!$table->hasColumn('token')) {
32+
$table->addColumn('token', Types::STRING, [
33+
'notnull' => false,
34+
'length' => 64
35+
]);
36+
}
37+
38+
if (!$table->hasColumn('password')) {
39+
$table->addColumn('password', Types::STRING, [
40+
'notnull' => false,
41+
'length' => 255
42+
]);
43+
}
44+
45+
if (!$table->hasIndex('shares_token_idx')) {
46+
$table->addIndex(['token'], 'shares_token_idx');
47+
}
48+
49+
return $schema;
50+
}
51+
}

0 commit comments

Comments
 (0)