Skip to content

Commit a1d007b

Browse files
committed
switch to registration via POST
- download relevant data is sent per POST, a token is received - download is started by GET and the received token - solves the disadvantages from XHR-only download - job to cleanup download tokens Signed-off-by: Arthur Schiwon <blizzz@arthur-schiwon.de>
1 parent 323fb60 commit a1d007b

7 files changed

Lines changed: 138 additions & 20 deletions

File tree

apps/files/appinfo/info.xml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
<job>OCA\Files\BackgroundJob\ScanFiles</job>
2727
<job>OCA\Files\BackgroundJob\DeleteOrphanedItems</job>
2828
<job>OCA\Files\BackgroundJob\CleanupFileLocks</job>
29+
<job>OCA\Files\BackgroundJob\CleanupDownloadTokens</job>
2930
<job>OCA\Files\BackgroundJob\CleanupDirectEditingTokens</job>
3031
</background-jobs>
3132

apps/files/appinfo/routes.php

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -101,10 +101,15 @@
101101
'url' => '/ajax/getstoragestats.php',
102102
'verb' => 'GET',
103103
],
104+
[
105+
'name' => 'ajax#registerDownload',
106+
'url' => '/registerDownload',
107+
'verb' => 'POST',
108+
],
104109
[
105110
'name' => 'ajax#download',
106111
'url' => '/ajax/download.php',
107-
'verb' => 'POST',
112+
'verb' => 'GET',
108113
],
109114
[
110115
'name' => 'API#toggleShowFolder',

apps/files/lib/AppInfo/Application.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,7 @@
6868

6969
class Application extends App implements IBootstrap {
7070
public const APP_ID = 'files';
71+
public const DL_TOKEN_PREFIX = 'dlToken_';
7172

7273
public function __construct(array $urlParams = []) {
7374
parent::__construct(self::APP_ID, $urlParams);
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
<?php
2+
declare(strict_types=1);
3+
/**
4+
* @copyright Copyright (c) 2021 Arthur Schiwon <blizzz@arthur-schiwon.de>
5+
*
6+
* @author Arthur Schiwon <blizzz@arthur-schiwon.de>
7+
*
8+
* @license GNU AGPL version 3 or any later version
9+
*
10+
* This program is free software: you can redistribute it and/or modify
11+
* it under the terms of the GNU Affero General Public License as
12+
* published by the Free Software Foundation, either version 3 of the
13+
* License, or (at your option) any later version.
14+
*
15+
* This program is distributed in the hope that it will be useful,
16+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
17+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
18+
* GNU Affero General Public License for more details.
19+
*
20+
* You should have received a copy of the GNU Affero General Public License
21+
* along with this program. If not, see <http://www.gnu.org/licenses/>.
22+
*
23+
*/
24+
25+
namespace OCA\files\lib\BackgroundJob;
26+
27+
use OC\BackgroundJob\TimedJob;
28+
use OCA\Files\AppInfo\Application;
29+
use OCP\IConfig;
30+
31+
class CleanupDownloadTokens extends TimedJob {
32+
private const INTERVAL_MINUTES = 24 * 60;
33+
/** @var IConfig */
34+
private $config;
35+
36+
public function __construct(IConfig $config) {
37+
$this->interval = self::INTERVAL_MINUTES;
38+
$this->config = $config;
39+
}
40+
41+
protected function run($argument) {
42+
$appKeys = $this->config->getAppKeys(Application::APP_ID);
43+
foreach ($appKeys as $key) {
44+
if (strpos($key, Application::DL_TOKEN_PREFIX) !== 0) {
45+
continue;
46+
}
47+
$dataStr = $this->config->getAppValue(Application::APP_ID, $key, '');
48+
if ($dataStr === '') {
49+
$this->config->deleteAppValue(Application::APP_ID, $key);
50+
continue;
51+
}
52+
$data = \json_decode($dataStr, true);
53+
if (!isset($data['lastActivity']) || (time() - $data['lastActivity']) > 24 * 60 * 2) {
54+
// deletes tokens that have not seen activity for 2 days
55+
// the period is chosen to allow continue of downloads with network interruptions in minde
56+
$this->config->deleteAppValue(Application::APP_ID, $key);
57+
}
58+
}
59+
}
60+
}

apps/files/lib/Controller/AjaxController.php

Lines changed: 61 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -27,21 +27,40 @@
2727
namespace OCA\Files\Controller;
2828

2929
use OC_Files;
30+
use OCA\Files\AppInfo\Application;
3031
use OCA\Files\Helper;
3132
use OCP\AppFramework\Controller;
3233
use OCP\AppFramework\Http\JSONResponse;
34+
use OCP\AppFramework\Http\NotFoundResponse;
3335
use OCP\Files\NotFoundException;
36+
use OCP\IConfig;
3437
use OCP\IRequest;
3538
use OCP\ISession;
39+
use OCP\Security\ISecureRandom;
40+
use function json_decode;
41+
use function json_encode;
3642

3743
class AjaxController extends Controller {
3844
/** @var ISession */
3945
private $session;
46+
/** @var IConfig */
47+
private $config;
4048

41-
public function __construct(string $appName, IRequest $request, ISession $session) {
49+
/** @var ISecureRandom */
50+
private $secureRandom;
51+
52+
public function __construct(
53+
string $appName,
54+
IRequest $request,
55+
ISession $session,
56+
IConfig $config,
57+
ISecureRandom $secureRandom
58+
) {
4259
parent::__construct($appName, $request);
4360
$this->session = $session;
4461
$this->request = $request;
62+
$this->config = $config;
63+
$this->secureRandom = $secureRandom;
4564
}
4665

4766
/**
@@ -67,26 +86,62 @@ public function getStorageStats(string $dir = '/'): JSONResponse {
6786
/**
6887
* @NoAdminRequired
6988
*/
70-
public function download($files, string $dir = '', string $downloadStartSecret = '') {
89+
public function registerDownload($files, string $dir = '', string $downloadStartSecret = '') {
7190
if (is_string($files)) {
7291
$files = [$files];
7392
} elseif (!is_array($files)) {
7493
throw new \InvalidArgumentException('Invalid argument for files');
7594
}
7695

96+
$attempts = 0;
97+
do {
98+
if ($attempts === 10) {
99+
throw new \RuntimeException('Failed to create unique download token');
100+
}
101+
$token = $this->secureRandom->generate(15);
102+
$attempts++;
103+
} while ($this->config->getAppValue(Application::APP_ID, Application::DL_TOKEN_PREFIX . $token, '') !== '');
104+
105+
$this->config->setAppValue(
106+
Application::APP_ID,
107+
Application::DL_TOKEN_PREFIX . $token,
108+
json_encode([
109+
'files' => $files,
110+
'dir' => $dir,
111+
'downloadStartSecret' => $downloadStartSecret,
112+
'lastActivity' => time()
113+
])
114+
);
115+
116+
return new JSONResponse(['token' => $token]);
117+
}
118+
119+
/**
120+
* @NoAdminRequired
121+
* @NoCSRFRequired
122+
*/
123+
public function download(string $token) {
124+
$dataStr = $this->config->getAppValue(Application::APP_ID, Application::DL_TOKEN_PREFIX . $token, '');
125+
if ($dataStr === '') {
126+
return new NotFoundResponse();
127+
}
77128
$this->session->close();
78129

79-
if (strlen($downloadStartSecret) <= 32
80-
&& (preg_match('!^[a-zA-Z0-9]+$!', $downloadStartSecret) === 1)
130+
$data = json_decode($dataStr, true);
131+
$data['lastActivity'] = time();
132+
$this->config->setAppValue(Application::APP_ID, Application::DL_TOKEN_PREFIX . $token, json_encode($data));
133+
134+
if (strlen($data['downloadStartSecret']) <= 32
135+
&& (preg_match('!^[a-zA-Z0-9]+$!', $data['downloadStartSecret']) === 1)
81136
) {
82-
setcookie('ocDownloadStarted', $downloadStartSecret, time() + 20, '/');
137+
setcookie('ocDownloadStarted', $data['downloadStartSecret'], time() + 20, '/');
83138
}
84139

85140
$serverParams = [ 'head' => $this->request->getMethod() === 'HEAD' ];
86141
if (isset($_SERVER['HTTP_RANGE'])) {
87142
$serverParams['range'] = $this->request->getHeader('Range');
88143
}
89144

90-
OC_Files::get($dir, $files, $serverParams);
145+
OC_Files::get($data['dir'], $data['files'], $serverParams);
91146
}
92147
}

apps/files/src/services/Download.js

Lines changed: 9 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -20,32 +20,29 @@
2020
*
2121
*/
2222

23-
import fileDownload from 'js-file-download'
2423
import { generateUrl } from '@nextcloud/router'
2524
import axios from '@nextcloud/axios'
2625

2726
export default class Download {
2827

2928
export
3029
const
31-
async get(files, dir, downloadStartSecret) {
32-
await axios.post(
33-
generateUrl('apps/files/ajax/download.php'),
30+
get(files, dir, downloadStartSecret) {
31+
axios.post(
32+
generateUrl('apps/files/registerDownload'),
3433
{
3534
files,
3635
dir,
3736
downloadStartSecret,
38-
},
39-
{
40-
responseType: 'blob',
4137
}
4238
).then(res => {
43-
const fileNameMatch = res.headers['content-disposition'].match(/filename="(.+)"/)
44-
let fileName = ''
45-
if (fileNameMatch.length === 2) {
46-
fileName = fileNameMatch[1]
39+
if (res.status === 200 && res.data.token) {
40+
const dlUrl = generateUrl(
41+
'apps/files/ajax/download.php?token={token}',
42+
{ token: res.data.token }
43+
)
44+
OC.redirect(dlUrl)
4745
}
48-
fileDownload(res.data, fileName)
4946
})
5047
}
5148

package.json

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,6 @@
5959
"jquery-migrate": "~3.3",
6060
"jquery-ui": "^1.12.1",
6161
"jquery-ui-dist": "^1.12.1",
62-
"js-file-download": "^0.4.12",
6362
"jstimezonedetect": "^1.0.7",
6463
"lodash": "^4.17.21",
6564
"marked": "^2.0.0",

0 commit comments

Comments
 (0)