diff --git a/apps/files/appinfo/info.xml b/apps/files/appinfo/info.xml index cefdfbae343a8..504de16303d74 100644 --- a/apps/files/appinfo/info.xml +++ b/apps/files/appinfo/info.xml @@ -26,6 +26,7 @@ OCA\Files\BackgroundJob\ScanFiles OCA\Files\BackgroundJob\DeleteOrphanedItems OCA\Files\BackgroundJob\CleanupFileLocks + OCA\Files\BackgroundJob\CleanupDownloadTokens OCA\Files\BackgroundJob\CleanupDirectEditingTokens diff --git a/apps/files/appinfo/routes.php b/apps/files/appinfo/routes.php index dc0ecadf35526..7103ee1f7ec0b 100644 --- a/apps/files/appinfo/routes.php +++ b/apps/files/appinfo/routes.php @@ -101,10 +101,15 @@ 'url' => '/ajax/getstoragestats.php', 'verb' => 'GET', ], + [ + 'name' => 'ajax#registerDownload', + 'url' => '/registerDownload', + 'verb' => 'POST', + ], [ 'name' => 'ajax#download', 'url' => '/ajax/download.php', - 'verb' => 'POST', + 'verb' => 'GET', ], [ 'name' => 'API#toggleShowFolder', diff --git a/apps/files/lib/AppInfo/Application.php b/apps/files/lib/AppInfo/Application.php index 92f29bfe410ad..25760d29b20b9 100644 --- a/apps/files/lib/AppInfo/Application.php +++ b/apps/files/lib/AppInfo/Application.php @@ -68,6 +68,7 @@ class Application extends App implements IBootstrap { public const APP_ID = 'files'; + public const DL_TOKEN_PREFIX = 'dlToken_'; public function __construct(array $urlParams = []) { parent::__construct(self::APP_ID, $urlParams); diff --git a/apps/files/lib/BackgroundJob/CleanupDownloadTokens.php b/apps/files/lib/BackgroundJob/CleanupDownloadTokens.php new file mode 100644 index 0000000000000..900cd46dcf08a --- /dev/null +++ b/apps/files/lib/BackgroundJob/CleanupDownloadTokens.php @@ -0,0 +1,60 @@ + + * + * @author Arthur Schiwon + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + */ + +namespace OCA\files\lib\BackgroundJob; + +use OC\BackgroundJob\TimedJob; +use OCA\Files\AppInfo\Application; +use OCP\IConfig; + +class CleanupDownloadTokens extends TimedJob { + private const INTERVAL_MINUTES = 24 * 60; + /** @var IConfig */ + private $config; + + public function __construct(IConfig $config) { + $this->interval = self::INTERVAL_MINUTES; + $this->config = $config; + } + + protected function run($argument) { + $appKeys = $this->config->getAppKeys(Application::APP_ID); + foreach ($appKeys as $key) { + if (strpos($key, Application::DL_TOKEN_PREFIX) !== 0) { + continue; + } + $dataStr = $this->config->getAppValue(Application::APP_ID, $key, ''); + if ($dataStr === '') { + $this->config->deleteAppValue(Application::APP_ID, $key); + continue; + } + $data = \json_decode($dataStr, true); + if (!isset($data['lastActivity']) || (time() - $data['lastActivity']) > 24 * 60 * 2) { + // deletes tokens that have not seen activity for 2 days + // the period is chosen to allow continue of downloads with network interruptions in minde + $this->config->deleteAppValue(Application::APP_ID, $key); + } + } + } +} diff --git a/apps/files/lib/Controller/AjaxController.php b/apps/files/lib/Controller/AjaxController.php index ef939df15ac46..8e0db6e4ddadb 100644 --- a/apps/files/lib/Controller/AjaxController.php +++ b/apps/files/lib/Controller/AjaxController.php @@ -27,21 +27,40 @@ namespace OCA\Files\Controller; use OC_Files; +use OCA\Files\AppInfo\Application; use OCA\Files\Helper; use OCP\AppFramework\Controller; use OCP\AppFramework\Http\JSONResponse; +use OCP\AppFramework\Http\NotFoundResponse; use OCP\Files\NotFoundException; +use OCP\IConfig; use OCP\IRequest; use OCP\ISession; +use OCP\Security\ISecureRandom; +use function json_decode; +use function json_encode; class AjaxController extends Controller { /** @var ISession */ private $session; + /** @var IConfig */ + private $config; - public function __construct(string $appName, IRequest $request, ISession $session) { + /** @var ISecureRandom */ + private $secureRandom; + + public function __construct( + string $appName, + IRequest $request, + ISession $session, + IConfig $config, + ISecureRandom $secureRandom + ) { parent::__construct($appName, $request); $this->session = $session; $this->request = $request; + $this->config = $config; + $this->secureRandom = $secureRandom; } /** @@ -67,19 +86,55 @@ public function getStorageStats(string $dir = '/'): JSONResponse { /** * @NoAdminRequired */ - public function download($files, string $dir = '', string $downloadStartSecret = '') { + public function registerDownload($files, string $dir = '', string $downloadStartSecret = '') { if (is_string($files)) { $files = [$files]; } elseif (!is_array($files)) { throw new \InvalidArgumentException('Invalid argument for files'); } + $attempts = 0; + do { + if ($attempts === 10) { + throw new \RuntimeException('Failed to create unique download token'); + } + $token = $this->secureRandom->generate(15); + $attempts++; + } while ($this->config->getAppValue(Application::APP_ID, Application::DL_TOKEN_PREFIX . $token, '') !== ''); + + $this->config->setAppValue( + Application::APP_ID, + Application::DL_TOKEN_PREFIX . $token, + json_encode([ + 'files' => $files, + 'dir' => $dir, + 'downloadStartSecret' => $downloadStartSecret, + 'lastActivity' => time() + ]) + ); + + return new JSONResponse(['token' => $token]); + } + + /** + * @NoAdminRequired + * @NoCSRFRequired + */ + public function download(string $token) { + $dataStr = $this->config->getAppValue(Application::APP_ID, Application::DL_TOKEN_PREFIX . $token, ''); + if ($dataStr === '') { + return new NotFoundResponse(); + } $this->session->close(); - if (strlen($downloadStartSecret) <= 32 - && (preg_match('!^[a-zA-Z0-9]+$!', $downloadStartSecret) === 1) + $data = json_decode($dataStr, true); + $data['lastActivity'] = time(); + $this->config->setAppValue(Application::APP_ID, Application::DL_TOKEN_PREFIX . $token, json_encode($data)); + + if (strlen($data['downloadStartSecret']) <= 32 + && (preg_match('!^[a-zA-Z0-9]+$!', $data['downloadStartSecret']) === 1) ) { - setcookie('ocDownloadStarted', $downloadStartSecret, time() + 20, '/'); + setcookie('ocDownloadStarted', $data['downloadStartSecret'], time() + 20, '/'); } $serverParams = [ 'head' => $this->request->getMethod() === 'HEAD' ]; @@ -87,6 +142,6 @@ public function download($files, string $dir = '', string $downloadStartSecret = $serverParams['range'] = $this->request->getHeader('Range'); } - OC_Files::get($dir, $files, $serverParams); + OC_Files::get($data['dir'], $data['files'], $serverParams); } } diff --git a/apps/files/src/services/Download.js b/apps/files/src/services/Download.js index 8451a9f8587a8..d49e2898dbb24 100644 --- a/apps/files/src/services/Download.js +++ b/apps/files/src/services/Download.js @@ -20,7 +20,6 @@ * */ -import fileDownload from 'js-file-download' import { generateUrl } from '@nextcloud/router' import axios from '@nextcloud/axios' @@ -28,24 +27,22 @@ export default class Download { export const - async get(files, dir, downloadStartSecret) { - await axios.post( - generateUrl('apps/files/ajax/download.php'), + get(files, dir, downloadStartSecret) { + axios.post( + generateUrl('apps/files/registerDownload'), { files, dir, downloadStartSecret, - }, - { - responseType: 'blob', } ).then(res => { - const fileNameMatch = res.headers['content-disposition'].match(/filename="(.+)"/) - let fileName = '' - if (fileNameMatch.length === 2) { - fileName = fileNameMatch[1] + if (res.status === 200 && res.data.token) { + const dlUrl = generateUrl( + 'apps/files/ajax/download.php?token={token}', + { token: res.data.token } + ) + OC.redirect(dlUrl) } - fileDownload(res.data, fileName) }) }