Skip to content

Commit 2a42dd4

Browse files
authored
Merge pull request #34564 from nextcloud/backport/34559/stable25
[stable25] Require token for local editing
2 parents 1fb7567 + 7da8714 commit 2a42dd4

File tree

10 files changed

+435
-6
lines changed

10 files changed

+435
-6
lines changed

apps/files/appinfo/info.xml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
<name>Files</name>
66
<summary>File Management</summary>
77
<description>File Management</description>
8-
<version>1.20.0</version>
8+
<version>1.20.1</version>
99
<licence>agpl</licence>
1010
<author>Robin Appelman</author>
1111
<author>Vincent Petry</author>
@@ -26,6 +26,7 @@
2626
<job>OCA\Files\BackgroundJob\DeleteOrphanedItems</job>
2727
<job>OCA\Files\BackgroundJob\CleanupFileLocks</job>
2828
<job>OCA\Files\BackgroundJob\CleanupDirectEditingTokens</job>
29+
<job>OCA\Files\BackgroundJob\DeleteExpiredOpenLocalEditor</job>
2930
</background-jobs>
3031

3132
<commands>

apps/files/appinfo/routes.php

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,8 @@
3737
*/
3838
namespace OCA\Files\AppInfo;
3939

40+
use OCA\Files\Controller\OpenLocalEditorController;
41+
4042
/** @var Application $application */
4143
$application = \OC::$server->query(Application::class);
4244
$application->registerRoutes(
@@ -169,6 +171,18 @@
169171
'url' => '/api/v1/transferownership/{id}',
170172
'verb' => 'DELETE',
171173
],
174+
[
175+
/** @see OpenLocalEditorController::create() */
176+
'name' => 'OpenLocalEditor#create',
177+
'url' => '/api/v1/openlocaleditor',
178+
'verb' => 'POST',
179+
],
180+
[
181+
/** @see OpenLocalEditorController::validate() */
182+
'name' => 'OpenLocalEditor#validate',
183+
'url' => '/api/v1/openlocaleditor/{token}',
184+
'verb' => 'POST',
185+
],
172186
],
173187
]
174188
);

apps/files/composer/composer/autoload_classmap.php

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
'OCA\\Files\\AppInfo\\Application' => $baseDir . '/../lib/AppInfo/Application.php',
2121
'OCA\\Files\\BackgroundJob\\CleanupDirectEditingTokens' => $baseDir . '/../lib/BackgroundJob/CleanupDirectEditingTokens.php',
2222
'OCA\\Files\\BackgroundJob\\CleanupFileLocks' => $baseDir . '/../lib/BackgroundJob/CleanupFileLocks.php',
23+
'OCA\\Files\\BackgroundJob\\DeleteExpiredOpenLocalEditor' => $baseDir . '/../lib/BackgroundJob/DeleteExpiredOpenLocalEditor.php',
2324
'OCA\\Files\\BackgroundJob\\DeleteOrphanedItems' => $baseDir . '/../lib/BackgroundJob/DeleteOrphanedItems.php',
2425
'OCA\\Files\\BackgroundJob\\ScanFiles' => $baseDir . '/../lib/BackgroundJob/ScanFiles.php',
2526
'OCA\\Files\\BackgroundJob\\TransferOwnership' => $baseDir . '/../lib/BackgroundJob/TransferOwnership.php',
@@ -35,9 +36,12 @@
3536
'OCA\\Files\\Controller\\ApiController' => $baseDir . '/../lib/Controller/ApiController.php',
3637
'OCA\\Files\\Controller\\DirectEditingController' => $baseDir . '/../lib/Controller/DirectEditingController.php',
3738
'OCA\\Files\\Controller\\DirectEditingViewController' => $baseDir . '/../lib/Controller/DirectEditingViewController.php',
39+
'OCA\\Files\\Controller\\OpenLocalEditorController' => $baseDir . '/../lib/Controller/OpenLocalEditorController.php',
3840
'OCA\\Files\\Controller\\TemplateController' => $baseDir . '/../lib/Controller/TemplateController.php',
3941
'OCA\\Files\\Controller\\TransferOwnershipController' => $baseDir . '/../lib/Controller/TransferOwnershipController.php',
4042
'OCA\\Files\\Controller\\ViewController' => $baseDir . '/../lib/Controller/ViewController.php',
43+
'OCA\\Files\\Db\\OpenLocalEditor' => $baseDir . '/../lib/Db/OpenLocalEditor.php',
44+
'OCA\\Files\\Db\\OpenLocalEditorMapper' => $baseDir . '/../lib/Db/OpenLocalEditorMapper.php',
4145
'OCA\\Files\\Db\\TransferOwnership' => $baseDir . '/../lib/Db/TransferOwnership.php',
4246
'OCA\\Files\\Db\\TransferOwnershipMapper' => $baseDir . '/../lib/Db/TransferOwnershipMapper.php',
4347
'OCA\\Files\\DirectEditingCapabilities' => $baseDir . '/../lib/DirectEditingCapabilities.php',
@@ -48,6 +52,7 @@
4852
'OCA\\Files\\Listener\\LegacyLoadAdditionalScriptsAdapter' => $baseDir . '/../lib/Listener/LegacyLoadAdditionalScriptsAdapter.php',
4953
'OCA\\Files\\Listener\\LoadSidebarListener' => $baseDir . '/../lib/Listener/LoadSidebarListener.php',
5054
'OCA\\Files\\Migration\\Version11301Date20191205150729' => $baseDir . '/../lib/Migration/Version11301Date20191205150729.php',
55+
'OCA\\Files\\Migration\\Version12101Date20221011153334' => $baseDir . '/../lib/Migration/Version12101Date20221011153334.php',
5156
'OCA\\Files\\Notification\\Notifier' => $baseDir . '/../lib/Notification/Notifier.php',
5257
'OCA\\Files\\Search\\FilesSearchProvider' => $baseDir . '/../lib/Search/FilesSearchProvider.php',
5358
'OCA\\Files\\Service\\DirectEditingService' => $baseDir . '/../lib/Service/DirectEditingService.php',

apps/files/composer/composer/autoload_static.php

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ class ComposerStaticInitFiles
3535
'OCA\\Files\\AppInfo\\Application' => __DIR__ . '/..' . '/../lib/AppInfo/Application.php',
3636
'OCA\\Files\\BackgroundJob\\CleanupDirectEditingTokens' => __DIR__ . '/..' . '/../lib/BackgroundJob/CleanupDirectEditingTokens.php',
3737
'OCA\\Files\\BackgroundJob\\CleanupFileLocks' => __DIR__ . '/..' . '/../lib/BackgroundJob/CleanupFileLocks.php',
38+
'OCA\\Files\\BackgroundJob\\DeleteExpiredOpenLocalEditor' => __DIR__ . '/..' . '/../lib/BackgroundJob/DeleteExpiredOpenLocalEditor.php',
3839
'OCA\\Files\\BackgroundJob\\DeleteOrphanedItems' => __DIR__ . '/..' . '/../lib/BackgroundJob/DeleteOrphanedItems.php',
3940
'OCA\\Files\\BackgroundJob\\ScanFiles' => __DIR__ . '/..' . '/../lib/BackgroundJob/ScanFiles.php',
4041
'OCA\\Files\\BackgroundJob\\TransferOwnership' => __DIR__ . '/..' . '/../lib/BackgroundJob/TransferOwnership.php',
@@ -50,9 +51,12 @@ class ComposerStaticInitFiles
5051
'OCA\\Files\\Controller\\ApiController' => __DIR__ . '/..' . '/../lib/Controller/ApiController.php',
5152
'OCA\\Files\\Controller\\DirectEditingController' => __DIR__ . '/..' . '/../lib/Controller/DirectEditingController.php',
5253
'OCA\\Files\\Controller\\DirectEditingViewController' => __DIR__ . '/..' . '/../lib/Controller/DirectEditingViewController.php',
54+
'OCA\\Files\\Controller\\OpenLocalEditorController' => __DIR__ . '/..' . '/../lib/Controller/OpenLocalEditorController.php',
5355
'OCA\\Files\\Controller\\TemplateController' => __DIR__ . '/..' . '/../lib/Controller/TemplateController.php',
5456
'OCA\\Files\\Controller\\TransferOwnershipController' => __DIR__ . '/..' . '/../lib/Controller/TransferOwnershipController.php',
5557
'OCA\\Files\\Controller\\ViewController' => __DIR__ . '/..' . '/../lib/Controller/ViewController.php',
58+
'OCA\\Files\\Db\\OpenLocalEditor' => __DIR__ . '/..' . '/../lib/Db/OpenLocalEditor.php',
59+
'OCA\\Files\\Db\\OpenLocalEditorMapper' => __DIR__ . '/..' . '/../lib/Db/OpenLocalEditorMapper.php',
5660
'OCA\\Files\\Db\\TransferOwnership' => __DIR__ . '/..' . '/../lib/Db/TransferOwnership.php',
5761
'OCA\\Files\\Db\\TransferOwnershipMapper' => __DIR__ . '/..' . '/../lib/Db/TransferOwnershipMapper.php',
5862
'OCA\\Files\\DirectEditingCapabilities' => __DIR__ . '/..' . '/../lib/DirectEditingCapabilities.php',
@@ -63,6 +67,7 @@ class ComposerStaticInitFiles
6367
'OCA\\Files\\Listener\\LegacyLoadAdditionalScriptsAdapter' => __DIR__ . '/..' . '/../lib/Listener/LegacyLoadAdditionalScriptsAdapter.php',
6468
'OCA\\Files\\Listener\\LoadSidebarListener' => __DIR__ . '/..' . '/../lib/Listener/LoadSidebarListener.php',
6569
'OCA\\Files\\Migration\\Version11301Date20191205150729' => __DIR__ . '/..' . '/../lib/Migration/Version11301Date20191205150729.php',
70+
'OCA\\Files\\Migration\\Version12101Date20221011153334' => __DIR__ . '/..' . '/../lib/Migration/Version12101Date20221011153334.php',
6671
'OCA\\Files\\Notification\\Notifier' => __DIR__ . '/..' . '/../lib/Notification/Notifier.php',
6772
'OCA\\Files\\Search\\FilesSearchProvider' => __DIR__ . '/..' . '/../lib/Search/FilesSearchProvider.php',
6873
'OCA\\Files\\Service\\DirectEditingService' => __DIR__ . '/..' . '/../lib/Service/DirectEditingService.php',

apps/files/js/filelist.js

Lines changed: 16 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2808,12 +2808,23 @@
28082808
},
28092809

28102810
openLocalClient: function(path) {
2811-
var scheme = 'nc://';
2812-
var command = 'open';
2813-
var uid = OC.getCurrentUser().uid;
2814-
var url = scheme + command + '/' + uid + '@' + window.location.host + OC.encodePath(path);
2811+
var link = OC.linkToOCS('apps/files/api/v1', 2) + 'openlocaleditor?format=json';
28152812

2816-
window.location.href = url;
2813+
$.post(link, {
2814+
path
2815+
})
2816+
.success(function(result) {
2817+
var scheme = 'nc://';
2818+
var command = 'open';
2819+
var uid = OC.getCurrentUser().uid;
2820+
var url = scheme + command + '/' + uid + '@' + window.location.host + OC.encodePath(path);
2821+
url += '?token=' + result.ocs.data.token;
2822+
2823+
window.location.href = url;
2824+
})
2825+
.fail(function() {
2826+
OC.Notification.show(t('files', 'Failed to redirect to client'))
2827+
})
28172828
},
28182829

28192830
/**
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
/**
6+
* @copyright Copyright (c) 2022 Joas Schilling <coding@schilljs.com>
7+
*
8+
* @author Joas Schilling <coding@schilljs.com>
9+
*
10+
* @license GNU AGPL version 3 or any later version
11+
*
12+
* This program is free software: you can redistribute it and/or modify
13+
* it under the terms of the GNU Affero General Public License as
14+
* published by the Free Software Foundation, either version 3 of the
15+
* License, or (at your option) any later version.
16+
*
17+
* This program is distributed in the hope that it will be useful,
18+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
19+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
20+
* GNU Affero General Public License for more details.
21+
*
22+
* You should have received a copy of the GNU Affero General Public License
23+
* along with this program. If not, see <http://www.gnu.org/licenses/>.
24+
*
25+
*/
26+
27+
namespace OCA\Files\BackgroundJob;
28+
29+
use OCA\Files\Controller\OpenLocalEditorController;
30+
use OCA\Files\Db\OpenLocalEditorMapper;
31+
use OCP\AppFramework\Utility\ITimeFactory;
32+
use OCP\BackgroundJob\IJob;
33+
use OCP\BackgroundJob\TimedJob;
34+
35+
/**
36+
* Delete all expired "Open local editor" token
37+
*/
38+
class DeleteExpiredOpenLocalEditor extends TimedJob {
39+
protected OpenLocalEditorMapper $mapper;
40+
41+
public function __construct(
42+
ITimeFactory $time,
43+
OpenLocalEditorMapper $mapper
44+
) {
45+
parent::__construct($time);
46+
$this->mapper = $mapper;
47+
48+
// Run every 12h
49+
$this->interval = 12 * 3600;
50+
$this->setTimeSensitivity(IJob::TIME_INSENSITIVE);
51+
}
52+
53+
/**
54+
* Makes the background job do its work
55+
*
56+
* @param array $argument unused argument
57+
*/
58+
public function run($argument): void {
59+
$this->mapper->deleteExpiredTokens($this->time->getTime());
60+
}
61+
}
Lines changed: 138 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,138 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
/**
6+
* @copyright Copyright (c) 2022 Joas Schilling <coding@schilljs.com>
7+
*
8+
* @author Joas Schilling <coding@schilljs.com>
9+
*
10+
* @license GNU AGPL version 3 or any later version
11+
*
12+
* This program is free software: you can redistribute it and/or modify
13+
* it under the terms of the GNU Affero General Public License as
14+
* published by the Free Software Foundation, either version 3 of the
15+
* License, or (at your option) any later version.
16+
*
17+
* This program is distributed in the hope that it will be useful,
18+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
19+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
20+
* GNU Affero General Public License for more details.
21+
*
22+
* You should have received a copy of the GNU Affero General Public License
23+
* along with this program. If not, see <http://www.gnu.org/licenses/>.
24+
*
25+
*/
26+
27+
namespace OCA\Files\Controller;
28+
29+
use OCA\Files\Db\OpenLocalEditor;
30+
use OCA\Files\Db\OpenLocalEditorMapper;
31+
use OCP\AppFramework\Db\DoesNotExistException;
32+
use OCP\AppFramework\Http;
33+
use OCP\AppFramework\Http\DataResponse;
34+
use OCP\AppFramework\OCSController;
35+
use OCP\AppFramework\Utility\ITimeFactory;
36+
use OCP\DB\Exception;
37+
use OCP\IRequest;
38+
use OCP\Security\ISecureRandom;
39+
use Psr\Log\LoggerInterface;
40+
41+
class OpenLocalEditorController extends OCSController {
42+
public const TOKEN_LENGTH = 128;
43+
public const TOKEN_DURATION = 600; // 10 Minutes
44+
public const TOKEN_RETRIES = 50;
45+
46+
protected ITimeFactory $timeFactory;
47+
protected OpenLocalEditorMapper $mapper;
48+
protected ISecureRandom $secureRandom;
49+
protected LoggerInterface $logger;
50+
protected ?string $userId;
51+
52+
public function __construct(
53+
string $appName,
54+
IRequest $request,
55+
ITimeFactory $timeFactory,
56+
OpenLocalEditorMapper $mapper,
57+
ISecureRandom $secureRandom,
58+
LoggerInterface $logger,
59+
?string $userId
60+
) {
61+
parent::__construct($appName, $request);
62+
63+
$this->timeFactory = $timeFactory;
64+
$this->mapper = $mapper;
65+
$this->secureRandom = $secureRandom;
66+
$this->logger = $logger;
67+
$this->userId = $userId;
68+
}
69+
70+
/**
71+
* @NoAdminRequired
72+
* @UserRateThrottle(limit=10, period=120)
73+
*/
74+
public function create(string $path): DataResponse {
75+
$pathHash = sha1($path);
76+
77+
$entity = new OpenLocalEditor();
78+
$entity->setUserId($this->userId);
79+
$entity->setPathHash($pathHash);
80+
$entity->setExpirationTime($this->timeFactory->getTime() + self::TOKEN_DURATION); // Expire in 10 minutes
81+
82+
for ($i = 1; $i <= self::TOKEN_RETRIES; $i++) {
83+
$token = $this->secureRandom->generate(self::TOKEN_LENGTH, ISecureRandom::CHAR_ALPHANUMERIC);
84+
$entity->setToken($token);
85+
86+
try {
87+
$this->mapper->insert($entity);
88+
89+
return new DataResponse([
90+
'userId' => $this->userId,
91+
'pathHash' => $pathHash,
92+
'expirationTime' => $entity->getExpirationTime(),
93+
'token' => $entity->getToken(),
94+
]);
95+
} catch (Exception $e) {
96+
if ($e->getCode() !== Exception::REASON_UNIQUE_CONSTRAINT_VIOLATION) {
97+
// Only retry on unique constraint violation
98+
throw $e;
99+
}
100+
}
101+
}
102+
103+
$this->logger->error('Giving up after ' . self::TOKEN_RETRIES . ' retries to generate a unique local editor token for path hash: ' . $pathHash);
104+
return new DataResponse([], Http::STATUS_INTERNAL_SERVER_ERROR);
105+
}
106+
107+
/**
108+
* @NoAdminRequired
109+
* @BruteForceProtection(action=openLocalEditor)
110+
*/
111+
public function validate(string $path, string $token): DataResponse {
112+
$pathHash = sha1($path);
113+
114+
try {
115+
$entity = $this->mapper->verifyToken($this->userId, $pathHash, $token);
116+
} catch (DoesNotExistException $e) {
117+
$response = new DataResponse([], Http::STATUS_NOT_FOUND);
118+
$response->throttle(['userId' => $this->userId, 'pathHash' => $pathHash]);
119+
return $response;
120+
}
121+
122+
$this->mapper->delete($entity);
123+
124+
if ($entity->getExpirationTime() <= $this->timeFactory->getTime()) {
125+
$response = new DataResponse([], Http::STATUS_NOT_FOUND);
126+
$response->throttle(['userId' => $this->userId, 'pathHash' => $pathHash]);
127+
return $response;
128+
}
129+
130+
return new DataResponse([
131+
'userId' => $this->userId,
132+
'pathHash' => $pathHash,
133+
'expirationTime' => $entity->getExpirationTime(),
134+
'token' => $entity->getToken(),
135+
]);
136+
}
137+
138+
}
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
/**
6+
* @copyright Copyright (c) 2022 Joas Schilling <coding@schilljs.com>
7+
*
8+
* @author Joas Schilling <coding@schilljs.com>
9+
*
10+
* @license GNU AGPL version 3 or any later version
11+
*
12+
* This program is free software: you can redistribute it and/or modify
13+
* it under the terms of the GNU Affero General Public License as
14+
* published by the Free Software Foundation, either version 3 of the
15+
* License, or (at your option) any later version.
16+
*
17+
* This program is distributed in the hope that it will be useful,
18+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
19+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
20+
* GNU Affero General Public License for more details.
21+
*
22+
* You should have received a copy of the GNU Affero General Public License
23+
* along with this program. If not, see <http://www.gnu.org/licenses/>.
24+
*
25+
*/
26+
27+
namespace OCA\Files\Db;
28+
29+
use OCP\AppFramework\Db\Entity;
30+
31+
/**
32+
* @method void setUserId(string $userId)
33+
* @method string getUserId()
34+
* @method void setPathHash(string $pathHash)
35+
* @method string getPathHash()
36+
* @method void setExpirationTime(int $expirationTime)
37+
* @method int getExpirationTime()
38+
* @method void setToken(string $token)
39+
* @method string getToken()
40+
*/
41+
class OpenLocalEditor extends Entity {
42+
/** @var string */
43+
protected $userId;
44+
45+
/** @var string */
46+
protected $pathHash;
47+
48+
/** @var int */
49+
protected $expirationTime;
50+
51+
/** @var string */
52+
protected $token;
53+
54+
public function __construct() {
55+
$this->addType('userId', 'string');
56+
$this->addType('pathHash', 'string');
57+
$this->addType('expirationTime', 'integer');
58+
$this->addType('token', 'string');
59+
}
60+
}

0 commit comments

Comments
 (0)