Merge pull request #9518 from nextcloud/feature/5986/public_share_controller_middleware

Public share middleware & controller
pull/9951/head
Roeland Jago Douma 7 years ago committed by GitHub
commit 8ebc3d90a0
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 8
      apps/files_sharing/appinfo/routes.php
  2. 11
      apps/files_sharing/js/public.js
  3. 44
      apps/files_sharing/lib/Controller/PublicPreviewController.php
  4. 169
      apps/files_sharing/lib/Controller/ShareController.php
  5. 23
      apps/files_sharing/lib/Middleware/SharingCheckMiddleware.php
  6. 18
      apps/files_sharing/tests/Controller/PublicPreviewControllerTest.php
  7. 226
      apps/files_sharing/tests/Controller/ShareControllerTest.php
  8. 89
      apps/files_sharing/tests/Middleware/SharingCheckMiddlewareTest.php
  9. 0
      core/css/publicshareauth.css
  10. 0
      core/js/publicshareauth.js
  11. 4
      core/templates/publicshareauth.php
  12. 4
      lib/composer/composer/autoload_classmap.php
  13. 4
      lib/composer/composer/autoload_static.php
  14. 8
      lib/private/AppFramework/DependencyInjection/DIContainer.php
  15. 7
      lib/private/AppFramework/Middleware/PublicShare/Exceptions/NeedAuthenticationException.php
  16. 112
      lib/private/AppFramework/Middleware/PublicShare/PublicShareMiddleware.php
  17. 2
      lib/private/legacy/template/functions.php
  18. 192
      lib/public/AppFramework/AuthPublicShareController.php
  19. 138
      lib/public/AppFramework/PublicShareController.php
  20. 4
      tests/acceptance/features/bootstrap/FilesSharingAppContext.php
  21. 159
      tests/lib/AppFramework/Controller/AuthPublicShareControllerTest.php
  22. 102
      tests/lib/AppFramework/Controller/PublicShareControllerTest.php
  23. 287
      tests/lib/AppFramework/Middleware/PublicShare/PublicShareMiddlewareTest.php

@ -34,13 +34,7 @@ return [
],
[
'name' => 'PublicPreview#getPreview',
'url' => '/publicpreview',
'verb' => 'GET',
],
[
'name' => 'PublicPreview#getPreview',
'url' => '/ajax/publicpreview.php',
'url' => '/publicpreview/{token}',
'verb' => 'GET',
],

@ -112,7 +112,6 @@ OCA.Sharing.PublicApp = {
y: Math.ceil(previewHeight * window.devicePixelRatio),
a: 'true',
file: encodeURIComponent(this.initialDir + $('#filename').val()),
t: token,
scalingup: 0
};
@ -150,7 +149,7 @@ OCA.Sharing.PublicApp = {
} else if ((previewSupported === 'true' && mimetype.substr(0, mimetype.indexOf('/')) !== 'video') ||
mimetype.substr(0, mimetype.indexOf('/')) === 'image' &&
mimetype !== 'image/svg+xml') {
img.attr('src', OC.filePath('files_sharing', 'ajax', 'publicpreview.php') + '?' + OC.buildQueryString(params));
img.attr('src', OC.linkTo('files_sharing', '/publicpreview/'+token) + '?' + OC.buildQueryString(params));
imgcontainer.appendTo('#imgframe');
} else if (mimetype.substr(0, mimetype.indexOf('/')) !== 'video') {
img.attr('src', OC.Util.replaceSVGIcon(mimetypeIcon));
@ -158,7 +157,7 @@ OCA.Sharing.PublicApp = {
imgcontainer.appendTo('#imgframe');
}
else if (previewSupported === 'true') {
$('#imgframe > video').attr('poster', OC.filePath('files_sharing', 'ajax', 'publicpreview.php') + '?' + OC.buildQueryString(params));
$('#imgframe > video').attr('poster', OC.generateUrl(OC.linkTo('files_sharing', '/publicpreview/'+token)) + '?' + OC.buildQueryString(params));
}
if (this.fileList) {
@ -223,8 +222,8 @@ OCA.Sharing.PublicApp = {
urlSpec.y *= window.devicePixelRatio;
urlSpec.x = Math.ceil(urlSpec.x);
urlSpec.y = Math.ceil(urlSpec.y);
urlSpec.t = $('#dirToken').val();
return OC.generateUrl('/apps/files_sharing/ajax/publicpreview.php?') + $.param(urlSpec);
var token = $('#dirToken').val();
return OC.generateUrl(OC.linkTo('files_sharing', '/publicpreview/'+token) + '?' + OC.buildQueryString(urlSpec));
};
this.fileList.updateEmptyContent = function() {
@ -427,4 +426,4 @@ $(document).ready(function () {
};
}
});
});

@ -27,15 +27,18 @@ use OCP\AppFramework\Controller;
use OCP\AppFramework\Http;
use OCP\AppFramework\Http\DataResponse;
use OCP\AppFramework\Http\FileDisplayResponse;
use OCP\AppFramework\PublicShareController;
use OCP\Constants;
use OCP\Files\Folder;
use OCP\Files\NotFoundException;
use OCP\IPreview;
use OCP\IRequest;
use OCP\ISession;
use OCP\Share\Exceptions\ShareNotFound;
use OCP\Share\IManager as ShareManager;
use OCP\Share\IShare;
class PublicPreviewController extends Controller {
class PublicPreviewController extends PublicShareController {
/** @var ShareManager */
private $shareManager;
@ -43,16 +46,38 @@ class PublicPreviewController extends Controller {
/** @var IPreview */
private $previewManager;
public function __construct($appName,
/** @var IShare */
private $share;
public function __construct(string $appName,
IRequest $request,
ShareManager $shareManger,
ISession $session,
IPreview $previewManager) {
parent::__construct($appName, $request);
parent::__construct($appName, $request, $session);
$this->shareManager = $shareManger;
$this->previewManager = $previewManager;
}
protected function getPasswordHash(): string {
return $this->share->getPassword();
}
public function isValidToken(): bool {
try {
$this->share = $this->shareManager->getShareByToken($this->getToken());
return true;
} catch (ShareNotFound $e) {
return false;
}
}
protected function isPasswordProtected(): bool {
return $this->share->getPassword() !== null;
}
/**
* @PublicPage
* @NoCSRFRequired
@ -60,24 +85,23 @@ class PublicPreviewController extends Controller {
* @param string $file
* @param int $x
* @param int $y
* @param string $t
* @param bool $a
* @return DataResponse|FileDisplayResponse
*/
public function getPreview(
$file = '',
$x = 32,
$y = 32,
$t = '',
string $token,
string $file = '',
int $x = 32,
int $y = 32,
$a = false
) {
if ($t === '' || $x === 0 || $y === 0) {
if ($token === '' || $x === 0 || $y === 0) {
return new DataResponse([], Http::STATUS_BAD_REQUEST);
}
try {
$share = $this->shareManager->getShareByToken($t);
$share = $this->shareManager->getShareByToken($token);
} catch (ShareNotFound $e) {
return new DataResponse([], Http::STATUS_NOT_FOUND);
}

@ -38,6 +38,7 @@ namespace OCA\Files_Sharing\Controller;
use OC_Files;
use OC_Util;
use OCA\FederatedFileSharing\FederatedShareProvider;
use OCP\AppFramework\AuthPublicShareController;
use OCP\AppFramework\Http\Template\SimpleMenuAction;
use OCP\AppFramework\Http\Template\ExternalShareMenuAction;
use OCP\AppFramework\Http\Template\LinkMenuAction;
@ -46,10 +47,8 @@ use OCP\Defaults;
use OCP\IL10N;
use OCP\Template;
use OCP\Share;
use OCP\AppFramework\Controller;
use OCP\IRequest;
use OCP\AppFramework\Http\TemplateResponse;
use OCP\AppFramework\Http\RedirectResponse;
use OCP\AppFramework\Http\NotFoundResponse;
use OCP\IURLGenerator;
use OCP\IConfig;
@ -58,32 +57,27 @@ use OCP\IUserManager;
use OCP\ISession;
use OCP\IPreview;
use OCA\Files_Sharing\Activity\Providers\Downloads;
use \OCP\Files\NotFoundException;
use OCP\Files\NotFoundException;
use OCP\Files\IRootFolder;
use OCP\Share\Exceptions\ShareNotFound;
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
use OCP\Share\IManager as ShareManager;
/**
* Class ShareController
*
* @package OCA\Files_Sharing\Controllers
*/
class ShareController extends Controller {
class ShareController extends AuthPublicShareController {
/** @var IConfig */
protected $config;
/** @var IURLGenerator */
protected $urlGenerator;
/** @var IUserManager */
protected $userManager;
/** @var ILogger */
protected $logger;
/** @var \OCP\Activity\IManager */
protected $activityManager;
/** @var \OCP\Share\IManager */
protected $shareManager;
/** @var ISession */
protected $session;
/** @var IPreview */
protected $previewManager;
/** @var IRootFolder */
@ -96,6 +90,11 @@ class ShareController extends Controller {
protected $l10n;
/** @var Defaults */
protected $defaults;
/** @var ShareManager */
protected $shareManager;
/** @var Share\IShare */
protected $share;
/**
* @param string $appName
@ -114,14 +113,14 @@ class ShareController extends Controller {
* @param IL10N $l10n
* @param Defaults $defaults
*/
public function __construct($appName,
public function __construct(string $appName,
IRequest $request,
IConfig $config,
IURLGenerator $urlGenerator,
IUserManager $userManager,
ILogger $logger,
\OCP\Activity\IManager $activityManager,
\OCP\Share\IManager $shareManager,
ShareManager $shareManager,
ISession $session,
IPreview $previewManager,
IRootFolder $rootFolder,
@ -129,108 +128,50 @@ class ShareController extends Controller {
EventDispatcherInterface $eventDispatcher,
IL10N $l10n,
Defaults $defaults) {
parent::__construct($appName, $request);
parent::__construct($appName, $request, $session, $urlGenerator);
$this->config = $config;
$this->urlGenerator = $urlGenerator;
$this->userManager = $userManager;
$this->logger = $logger;
$this->activityManager = $activityManager;
$this->shareManager = $shareManager;
$this->session = $session;
$this->previewManager = $previewManager;
$this->rootFolder = $rootFolder;
$this->federatedShareProvider = $federatedShareProvider;
$this->eventDispatcher = $eventDispatcher;
$this->l10n = $l10n;
$this->defaults = $defaults;
$this->shareManager = $shareManager;
}
/**
* @PublicPage
* @NoCSRFRequired
*
* @param string $token
* @return TemplateResponse|RedirectResponse
*/
public function showAuthenticate($token) {
$share = $this->shareManager->getShareByToken($token);
if($this->linkShareAuth($share)) {
return new RedirectResponse($this->urlGenerator->linkToRoute('files_sharing.sharecontroller.showShare', array('token' => $token)));
}
return new TemplateResponse($this->appName, 'authenticate', array(), 'guest');
protected function verifyPassword(string $password): bool {
return $this->shareManager->checkPassword($this->share, $password);
}
/**
* @PublicPage
* @UseSession
* @BruteForceProtection(action=publicLinkAuth)
*
* Authenticates against password-protected shares
* @param string $token
* @param string $redirect
* @param string $password
* @return RedirectResponse|TemplateResponse|NotFoundResponse
*/
public function authenticate($token, $redirect, $password = '') {
protected function getPasswordHash(): string {
return $this->share->getPassword();
}
// Check whether share exists
public function isValidToken(): bool {
try {
$share = $this->shareManager->getShareByToken($token);
$this->share = $this->shareManager->getShareByToken($this->getToken());
} catch (ShareNotFound $e) {
return new NotFoundResponse();
return false;
}
$authenticate = $this->linkShareAuth($share, $password);
// if download was requested before auth, redirect to download
if ($authenticate === true && $redirect === 'download') {
return new RedirectResponse($this->urlGenerator->linkToRoute(
'files_sharing.sharecontroller.downloadShare',
array('token' => $token))
);
} else if ($authenticate === true) {
return new RedirectResponse($this->urlGenerator->linkToRoute(
'files_sharing.sharecontroller.showShare',
array('token' => $token))
);
}
return true;
}
$response = new TemplateResponse($this->appName, 'authenticate', array('wrongpw' => true), 'guest');
$response->throttle();
return $response;
protected function isPasswordProtected(): bool {
return $this->share->getPassword() !== null;
}
/**
* Authenticate a link item with the given password.
* Or use the session if no password is provided.
*
* This is a modified version of Helper::authenticate
* TODO: Try to merge back eventually with Helper::authenticate
*
* @param \OCP\Share\IShare $share
* @param string|null $password
* @return bool
*/
private function linkShareAuth(\OCP\Share\IShare $share, $password = null) {
if ($password !== null) {
if ($this->shareManager->checkPassword($share, $password)) {
$this->session->regenerateId(true, true);
$this->session->set('public_link_authenticated', (string)$share->getId());
} else {
$this->emitAccessShareHook($share, 403, 'Wrong password');
return false;
}
} else {
// not authenticated ?
if ( ! $this->session->exists('public_link_authenticated')
|| $this->session->get('public_link_authenticated') !== (string)$share->getId()) {
return false;
}
}
return true;
protected function authSucceeded() {
// For share this was always set so it is still used in other apps
$this->session->set('public_link_authenticated', (string)$this->share->getId());
}
protected function authFailed() {
$this->emitAccessShareHook($this->share, 403, 'Wrong password');
}
/**
@ -285,27 +226,21 @@ class ShareController extends Controller {
* @PublicPage
* @NoCSRFRequired
*
* @param string $token
* @param string $path
* @return TemplateResponse|RedirectResponse|NotFoundResponse
* @return TemplateResponse
* @throws NotFoundException
* @throws \Exception
*/
public function showShare($token, $path = '') {
public function showShare($path = ''): TemplateResponse {
\OC_User::setIncognitoMode(true);
// Check whether share exists
try {
$share = $this->shareManager->getShareByToken($token);
$share = $this->shareManager->getShareByToken($this->getToken());
} catch (ShareNotFound $e) {
$this->emitAccessShareHook($token, 404, 'Share not found');
return new NotFoundResponse();
}
// Share is password protected - check whether the user is permitted to access the share
if ($share->getPassword() !== null && !$this->linkShareAuth($share)) {
return new RedirectResponse($this->urlGenerator->linkToRoute('files_sharing.sharecontroller.authenticate',
array('token' => $token, 'redirect' => 'preview')));
$this->emitAccessShareHook($this->getToken(), 404, 'Share not found');
throw new NotFoundException();
}
if (!$this->validateShare($share)) {
@ -329,8 +264,8 @@ class ShareController extends Controller {
$shareTmpl['directory_path'] = $share->getTarget();
$shareTmpl['mimetype'] = $share->getNode()->getMimetype();
$shareTmpl['previewSupported'] = $this->previewManager->isMimeSupported($share->getNode()->getMimetype());
$shareTmpl['dirToken'] = $token;
$shareTmpl['sharingToken'] = $token;
$shareTmpl['dirToken'] = $this->getToken();
$shareTmpl['sharingToken'] = $this->getToken();
$shareTmpl['server2serversharing'] = $this->federatedShareProvider->isOutgoingServer2serverShareEnabled();
$shareTmpl['protected'] = $share->getPassword() !== null ? 'true' : 'false';
$shareTmpl['dir'] = '';
@ -367,7 +302,7 @@ class ShareController extends Controller {
$folder = new Template('files', 'list', '');
$folder->assign('dir', $rootFolder->getRelativePath($folderNode->getPath()));
$folder->assign('dirToken', $token);
$folder->assign('dirToken', $this->getToken());
$folder->assign('permissions', \OCP\Constants::PERMISSION_READ);
$folder->assign('isPublic', true);
$folder->assign('hideFileList', $hideFileList);
@ -382,8 +317,8 @@ class ShareController extends Controller {
$shareTmpl['hideFileList'] = $hideFileList;
$shareTmpl['shareOwner'] = $this->userManager->get($share->getShareOwner())->getDisplayName();
$shareTmpl['downloadURL'] = $this->urlGenerator->linkToRouteAbsolute('files_sharing.sharecontroller.downloadShare', ['token' => $token]);
$shareTmpl['shareUrl'] = $this->urlGenerator->linkToRouteAbsolute('files_sharing.sharecontroller.showShare', ['token' => $token]);
$shareTmpl['downloadURL'] = $this->urlGenerator->linkToRouteAbsolute('files_sharing.sharecontroller.downloadShare', ['token' => $this->getToken()]);
$shareTmpl['shareUrl'] = $this->urlGenerator->linkToRouteAbsolute('files_sharing.sharecontroller.showShare', ['token' => $this->getToken()]);
$shareTmpl['maxSizeAnimateGif'] = $this->config->getSystemValue('max_filesize_animated_gifs_public_sharing', 10);
$shareTmpl['previewEnabled'] = $this->config->getSystemValue('enable_previews', true);
$shareTmpl['previewMaxX'] = $this->config->getSystemValue('preview_max_x', 1024);
@ -393,19 +328,19 @@ class ShareController extends Controller {
$ogPreview = '';
if ($shareTmpl['previewSupported']) {
$shareTmpl['previewImage'] = $this->urlGenerator->linkToRouteAbsolute( 'files_sharing.PublicPreview.getPreview',
['x' => 200, 'y' => 200, 'file' => $shareTmpl['directory_path'], 't' => $shareTmpl['dirToken']]);
['x' => 200, 'y' => 200, 'file' => $shareTmpl['directory_path'], 'token' => $shareTmpl['dirToken']]);
$ogPreview = $shareTmpl['previewImage'];
// We just have direct previews for image files
if ($share->getNode()->getMimePart() === 'image') {
$shareTmpl['previewURL'] = $this->urlGenerator->linkToRouteAbsolute('files_sharing.publicpreview.directLink', ['token' => $token]);
$shareTmpl['previewURL'] = $this->urlGenerator->linkToRouteAbsolute('files_sharing.publicpreview.directLink', ['token' => $this->getToken()]);
$ogPreview = $shareTmpl['previewURL'];
//Whatapp is kind of picky about their size requirements
if ($this->request->isUserAgent(['/^WhatsApp/'])) {
$ogPreview = $this->urlGenerator->linkToRouteAbsolute('files_sharing.PublicPreview.getPreview', [
't' => $token,
'token' => $this->getToken(),
'x' => 256,
'y' => 256,
'a' => true,
@ -488,12 +423,6 @@ class ShareController extends Controller {
return new \OCP\AppFramework\Http\DataResponse('Share is read-only');
}
// Share is password protected - check whether the user is permitted to access the share
if ($share->getPassword() !== null && !$this->linkShareAuth($share)) {
return new RedirectResponse($this->urlGenerator->linkToRoute('files_sharing.sharecontroller.authenticate',
['token' => $token, 'redirect' => 'download']));
}
$files_list = null;
if (!is_null($files)) { // download selected files
$files_list = json_decode($files);
@ -507,13 +436,15 @@ class ShareController extends Controller {
}
}
$userFolder = $this->rootFolder->getUserFolder($share->getShareOwner());
$originalSharePath = $userFolder->getRelativePath($share->getNode()->getPath());
if (!$this->validateShare($share)) {
throw new NotFoundException();
}
$userFolder = $this->rootFolder->getUserFolder($share->getShareOwner());
$originalSharePath = $userFolder->getRelativePath($share->getNode()->getPath());
// Single file share
if ($share->getNode() instanceof \OCP\Files\File) {
// Single file download

@ -101,13 +101,6 @@ class SharingCheckMiddleware extends Middleware {
if ($controller instanceof ExternalSharesController &&
!$this->externalSharesChecks()) {
throw new S2SException('Federated sharing not allowed');
} else if ($controller instanceof ShareController) {
$token = $this->request->getParam('token');
$share = $this->shareManager->getShareByToken($token);
if ($share->getShareType() === \OCP\Share::SHARE_TYPE_LINK
&& !$this->isLinkSharingEnabled()) {
throw new NotFoundException('Link sharing is disabled');
}
}
}
@ -165,22 +158,6 @@ class SharingCheckMiddleware extends Middleware {
return true;
}
/**
* Check if link sharing is allowed
* @return bool
*/
private function isLinkSharingEnabled() {
// Check if the shareAPI is enabled
if ($this->config->getAppValue('core', 'shareapi_enabled', 'yes') !== 'yes') {
return false;
}
// Check whether public sharing is enabled
if($this->config->getAppValue('core', 'shareapi_allow_links', 'yes') !== 'yes') {
return false;
}
return true;
}
}

@ -33,6 +33,7 @@ use OCP\Files\NotFoundException;
use OCP\Files\SimpleFS\ISimpleFile;
use OCP\IPreview;
use OCP\IRequest;
use OCP\ISession;
use OCP\Share\Exceptions\ShareNotFound;
use OCP\Share\IManager;
use OCP\Share\IShare;
@ -60,26 +61,27 @@ class PublicPreviewControllerTest extends TestCase {
'files_sharing',
$this->createMock(IRequest::class),
$this->shareManager,
$this->createMock(ISession::class),
$this->previewManager
);
}
public function testInvalidToken() {
$res = $this->controller->getPreview('file', 10, 10, '');
$res = $this->controller->getPreview('', 'file', 10, 10, '');
$expected = new DataResponse([], Http::STATUS_BAD_REQUEST);
$this->assertEquals($expected, $res);
}
public function testInvalidWidth() {
$res = $this->controller->getPreview('file', 0);
$res = $this->controller->getPreview('token', 'file', 0);
$expected = new DataResponse([], Http::STATUS_BAD_REQUEST);
$this->assertEquals($expected, $res);
}
public function testInvalidHeight() {
$res = $this->controller->getPreview('file', 10, 0);
$res = $this->controller->getPreview('token', 'file', 10, 0);
$expected = new DataResponse([], Http::STATUS_BAD_REQUEST);
$this->assertEquals($expected, $res);
@ -90,7 +92,7 @@ class PublicPreviewControllerTest extends TestCase {
->with($this->equalTo('token'))
->willThrowException(new ShareNotFound());
$res = $this->controller->getPreview('file', 10, 10, 'token');
$res = $this->controller->getPreview('token', 'file', 10, 10);
$expected = new DataResponse([], Http::STATUS_NOT_FOUND);
$this->assertEquals($expected, $res);
@ -105,7 +107,7 @@ class PublicPreviewControllerTest extends TestCase {
$share->method('getPermissions')
->willReturn(0);
$res = $this->controller->getPreview('file', 10, 10, 'token');
$res = $this->controller->getPreview('token', 'file', 10, 10);
$expected = new DataResponse([], Http::STATUS_FORBIDDEN);
$this->assertEquals($expected, $res);
@ -132,7 +134,7 @@ class PublicPreviewControllerTest extends TestCase {
$preview->method('getMimeType')
->willReturn('myMime');
$res = $this->controller->getPreview('file', 10, 10, 'token', true);
$res = $this->controller->getPreview('token', 'file', 10, 10, true);
$expected = new FileDisplayResponse($preview, Http::STATUS_OK, ['Content-Type' => 'myMime']);
$this->assertEquals($expected, $res);
}
@ -154,7 +156,7 @@ class PublicPreviewControllerTest extends TestCase {
->with($this->equalTo('file'))
->willThrowException(new NotFoundException());
$res = $this->controller->getPreview('file', 10, 10, 'token', true);
$res = $this->controller->getPreview('token', 'file', 10, 10, true);
$expected = new DataResponse([], Http::STATUS_NOT_FOUND);
$this->assertEquals($expected, $res);
}
@ -186,7 +188,7 @@ class PublicPreviewControllerTest extends TestCase {
$preview->method('getMimeType')
->willReturn('myMime');
$res = $this->controller->getPreview('file', 10, 10, 'token', true);
$res = $this->controller->getPreview('token', 'file', 10, 10, true);
$expected = new FileDisplayResponse($preview, Http::STATUS_OK, ['Content-Type' => 'myMime']);
$this->assertEquals($expected, $res);
}

@ -39,6 +39,7 @@ use OCP\AppFramework\Http\Template\ExternalShareMenuAction;
use OCP\AppFramework\Http\Template\LinkMenuAction;
use OCP\AppFramework\Http\Template\PublicTemplateResponse;
use OCP\AppFramework\Http\Template\SimpleMenuAction;
use OCP\Files\NotFoundException;
use OCP\IConfig;
use OCP\IL10N;
use OCP\ILogger;
@ -156,193 +157,24 @@ class ShareControllerTest extends \Test\TestCase {
parent::tearDown();
}
public function testShowAuthenticateNotAuthenticated() {
$share = \OC::$server->getShareManager()->newShare();
$this->shareManager
->expects($this->once())
->method('getShareByToken')
->with('token')
->willReturn($share);
$response = $this->shareController->showAuthenticate('token');
$expectedResponse = new TemplateResponse($this->appName, 'authenticate', [], 'guest');
$this->assertEquals($expectedResponse, $response);
}
public function testShowAuthenticateAuthenticatedForDifferentShare() {
$share = \OC::$server->getShareManager()->newShare();
$share->setId(1);
$this->shareManager
->expects($this->once())
->method('getShareByToken')
->with('token')
->willReturn($share);
$this->session->method('exists')->with('public_link_authenticated')->willReturn(true);
$this->session->method('get')->with('public_link_authenticated')->willReturn('2');
$response = $this->shareController->showAuthenticate('token');
$expectedResponse = new TemplateResponse($this->appName, 'authenticate', [], 'guest');
$this->assertEquals($expectedResponse, $response);
}
public function testShowAuthenticateCorrectShare() {
$share = \OC::$server->getShareManager()->newShare();
$share->setId(1);
$this->shareManager
->expects($this->once())
->method('getShareByToken')
->with('token')
->willReturn($share);
$this->session->method('exists')->with('public_link_authenticated')->willReturn(true);
$this->session->method('get')->with('public_link_authenticated')->willReturn('1');
$this->urlGenerator->expects($this->once())
->method('linkToRoute')
->with('files_sharing.sharecontroller.showShare', ['token' => 'token'])
->willReturn('redirect');
$response = $this->shareController->showAuthenticate('token');
$expectedResponse = new RedirectResponse('redirect');
$this->assertEquals($expectedResponse, $response);
}
public function testAuthenticateInvalidToken() {
$this->shareManager
->expects($this->once())
->method('getShareByToken')
->with('token')
->will($this->throwException(new \OCP\Share\Exceptions\ShareNotFound()));
$response = $this->shareController->authenticate('token', 'preview');
$expectedResponse = new NotFoundResponse();
$this->assertEquals($expectedResponse, $response);
}
public function testAuthenticateValidPassword() {
$share = \OC::$server->getShareManager()->newShare();
$share->setId(42);
$this->shareManager
->expects($this->once())
->method('getShareByToken')
->with('token')
->willReturn($share);
$this->shareManager
->expects($this->once())
->method('checkPassword')
->with($share, 'validpassword')
->willReturn(true);
$this->session
->expects($this->once())
->method('set')
->with('public_link_authenticated', '42');
$this->urlGenerator->expects($this->once())
->method('linkToRoute')
->with('files_sharing.sharecontroller.showShare', ['token'=>'token'])
->willReturn('redirect');
$response = $this->shareController->authenticate('token', 'preview', 'validpassword');
$expectedResponse = new RedirectResponse('redirect');
$this->assertEquals($expectedResponse, $response);
}
public function testAuthenticateValidPasswordAndDownload() {
$share = \OC::$server->getShareManager()->newShare();
$share->setId(42);
$this->shareManager
->expects($this->once())
->method('getShareByToken')
->with('token')
->willReturn($share);
$this->shareManager
->expects($this->once())
->method('checkPassword')
->with($share, 'validpassword')
->willReturn(true);
$this->session
->expects($this->once())
->method('set')
->with('public_link_authenticated', '42');
$this->urlGenerator->expects($this->once())
->method('linkToRoute')
->with('files_sharing.sharecontroller.downloadShare', ['token'=>'token'])
->willReturn('redirect');
$response = $this->shareController->authenticate('token', 'download', 'validpassword');
$expectedResponse = new RedirectResponse('redirect');
$this->assertEquals($expectedResponse, $response);
}
public function testAuthenticateInvalidPassword() {
$share = \OC::$server->getShareManager()->newShare();
$share->setNodeId(100)
->setNodeType('file')
->setToken('token')
->setSharedBy('initiator')
->setId(42);
$this->shareManager
->expects($this->once())
->method('getShareByToken')
->with('token')
->willReturn($share);
$this->shareManager
->expects($this->once())
->method('checkPassword')
->with($share, 'invalidpassword')
->willReturn(false);
$this->session
->expects($this->never())
->method('set');
$hookListner = $this->getMockBuilder('Dummy')->setMethods(['access'])->getMock();
\OCP\Util::connectHook('OCP\Share', 'share_link_access', $hookListner, 'access');
$hookListner->expects($this->once())
->method('access')
->with($this->callback(function(array $data) {
return $data['itemType'] === 'file' &&
$data['itemSource'] === 100 &&
$data['uidOwner'] === 'initiator' &&
$data['token'] === 'token' &&
$data['errorCode'] === 403 &&
$data['errorMessage'] === 'Wrong password';
}));
$response = $this->shareController->authenticate('token', 'preview', 'invalidpassword');
$expectedResponse = new TemplateResponse($this->appName, 'authenticate', array('wrongpw' => true), 'guest');
$expectedResponse->throttle();
$this->assertEquals($expectedResponse, $response);
}
public function testShowShareInvalidToken() {
$this->shareController->setToken('invalidtoken');
$this->shareManager
->expects($this->once())
->method('getShareByToken')
->with('invalidtoken')
->will($this->throwException(new ShareNotFound()));
$this->expectException(NotFoundException::class);
// Test without a not existing token
$response = $this->shareController->showShare('invalidtoken');
$expectedResponse = new NotFoundResponse();
$this->assertEquals($expectedResponse, $response);
$this->shareController->showShare();
}
public function testShowShareNotAuthenticated() {
$this->shareController->setToken('validtoken');
$share = \OC::$server->getShareManager()->newShare();
$share->setPassword('password');
@ -352,19 +184,16 @@ class ShareControllerTest extends \Test\TestCase {
->with('validtoken')
->willReturn($share);
$this->urlGenerator->expects($this->once())
->method('linkToRoute')
->with('files_sharing.sharecontroller.authenticate', ['token' => 'validtoken', 'redirect' => 'preview'])
->willReturn('redirect');
$this->expectException(NotFoundException::class);
// Test without a not existing token
$response = $this->shareController->showShare('validtoken');
$expectedResponse = new RedirectResponse('redirect');
$this->assertEquals($expectedResponse, $response);
$this->shareController->showShare();
}
public function testShowShare() {
$this->shareController->setToken('token');
$owner = $this->getMockBuilder(IUser::class)->getMock();
$owner->method('getDisplayName')->willReturn('ownerDisplay');
$owner->method('getUID')->willReturn('ownerUID');
@ -428,7 +257,7 @@ class ShareControllerTest extends \Test\TestCase {
return vsprintf($text, $parameters);
}));
$response = $this->shareController->showShare('token');
$response = $this->shareController->showShare();
$sharedTmplParams = array(
'displayName' => 'ownerDisplay',
'owner' => 'ownerUID',
@ -476,6 +305,8 @@ class ShareControllerTest extends \Test\TestCase {
* @expectedException \OCP\Files\NotFoundException
*/
public function testShowShareInvalid() {
$this->shareController->setToken('token');
$owner = $this->getMockBuilder(IUser::class)->getMock();
$owner->method('getDisplayName')->willReturn('ownerDisplay');
$owner->method('getUID')->willReturn('ownerUID');
@ -517,32 +348,7 @@ class ShareControllerTest extends \Test\TestCase {
$this->userManager->method('get')->with('ownerUID')->willReturn($owner);
$this->shareController->showShare('token');
}
public function testDownloadShare() {
$share = $this->getMockBuilder(IShare::class)->getMock();
$share->method('getPassword')->willReturn('password');
$share
->expects($this->once())
->method('getPermissions')
->willReturn(\OCP\Constants::PERMISSION_READ);
$this->shareManager
->expects($this->once())
->method('getShareByToken')
->with('validtoken')
->willReturn($share);
$this->urlGenerator->expects($this->once())
->method('linkToRoute')
->with('files_sharing.sharecontroller.authenticate', ['token' => 'validtoken', 'redirect' => 'download'])
->willReturn('redirect');
// Test with a password protected share and no authentication
$response = $this->shareController->downloadShare('validtoken');
$expectedResponse = new RedirectResponse('redirect');
$this->assertEquals($expectedResponse, $response);
$this->shareController->showShare();
}
public function testDownloadShareWithCreateOnlyShare() {

@ -98,49 +98,6 @@ class SharingCheckMiddlewareTest extends \Test\TestCase {
$this->assertFalse(self::invokePrivate($this->sharingCheckMiddleware, 'isSharingEnabled'));
}
public function testIsLinkSharingEnabledWithEverythinEnabled() {
$this->config
->expects($this->at(0))
->method('getAppValue')
->with('core', 'shareapi_enabled', 'yes')
->will($this->returnValue('yes'));
$this->config
->expects($this->at(1))
->method('getAppValue')
->with('core', 'shareapi_allow_links', 'yes')
->will($this->returnValue('yes'));
$this->assertTrue(self::invokePrivate($this->sharingCheckMiddleware, 'isLinkSharingEnabled'));
}
public function testIsLinkSharingEnabledWithLinkSharingDisabled() {
$this->config
->expects($this->at(0))
->method('getAppValue')
->with('core', 'shareapi_enabled', 'yes')
->will($this->returnValue('yes'));
$this->config
->expects($this->at(1))
->method('getAppValue')
->with('core', 'shareapi_allow_links', 'yes')
->will($this->returnValue('no'));
$this->assertFalse(self::invokePrivate($this->sharingCheckMiddleware, 'isLinkSharingEnabled'));
}
public function testIsLinkSharingEnabledWithSharingAPIDisabled() {
$this->config
->expects($this->once())
->method('getAppValue')
->with('core', 'shareapi_enabled', 'yes')
->will($this->returnValue('no'));
$this->assertFalse(self::invokePrivate($this->sharingCheckMiddleware, 'isLinkSharingEnabled'));
}
public function externalSharesChecksDataProvider() {
$data = [];
@ -236,57 +193,11 @@ class SharingCheckMiddlewareTest extends \Test\TestCase {
->with('files_sharing')
->will($this->returnValue(true));
$this->config
->expects($this->at(0))
->method('getAppValue')
->with('core', 'shareapi_enabled', 'yes')
->will($this->returnValue('yes'));
$this->config
->expects($this->at(1))
->method('getAppValue')
->with('core', 'shareapi_allow_links', 'yes')
->will($this->returnValue('yes'));
$this->request->expects($this->once())->method('getParam')->with('token')
->willReturn('token');
$this->shareManager->expects($this->once())->method('getShareByToken')
->with('token')->willReturn($share);
$share->expects($this->once())->method('getShareType')->willReturn(\OCP\Share::SHARE_TYPE_LINK);
$controller = $this->createMock(ShareController::class);
$this->sharingCheckMiddleware->beforeController($controller, 'myMethod');
}
/**
* @expectedException \OCP\Files\NotFoundException
* @expectedExceptionMessage Link sharing is disabled
*/
public function testBeforeControllerWithShareControllerWithSharingEnabledAPIDisabled() {
$share = $this->createMock(IShare::class);
$this->appManager
->expects($this->once())
->method('isEnabledForUser')
->with('files_sharing')
->will($this->returnValue(true));
$controller = $this->createMock(ShareController::class);
$this->request->expects($this->once())->method('getParam')->with('token')
->willReturn('token');
$this->shareManager->expects($this->once())->method('getShareByToken')
->with('token')->willReturn($share);
$share->expects($this->once())->method('getShareType')->willReturn(\OCP\Share::SHARE_TYPE_LINK);
$this->sharingCheckMiddleware->beforeController($controller, 'myMethod');
}
/**
* @expectedException \OCP\Files\NotFoundException
* @expectedExceptionMessage Sharing is disabled.

@ -2,8 +2,8 @@
/** @var $_ array */
/** @var $l \OCP\IL10N */
style('core', 'guest');
style('files_sharing', 'authenticate');
script('files_sharing', 'authenticate');
style('core', 'publicshareauth');
script('core', 'publicshareauth');
?>
<form method="post">
<fieldset class="warning">

@ -18,6 +18,7 @@ return array(
'OCP\\App' => $baseDir . '/lib/public/App.php',
'OCP\\AppFramework\\ApiController' => $baseDir . '/lib/public/AppFramework/ApiController.php',
'OCP\\AppFramework\\App' => $baseDir . '/lib/public/AppFramework/App.php',
'OCP\\AppFramework\\AuthPublicShareController' => $baseDir . '/lib/public/AppFramework/AuthPublicShareController.php',
'OCP\\AppFramework\\Controller' => $baseDir . '/lib/public/AppFramework/Controller.php',
'OCP\\AppFramework\\Db\\DoesNotExistException' => $baseDir . '/lib/public/AppFramework/Db/DoesNotExistException.php',
'OCP\\AppFramework\\Db\\Entity' => $baseDir . '/lib/public/AppFramework/Db/Entity.php',
@ -56,6 +57,7 @@ return array(
'OCP\\AppFramework\\OCS\\OCSException' => $baseDir . '/lib/public/AppFramework/OCS/OCSException.php',
'OCP\\AppFramework\\OCS\\OCSForbiddenException' => $baseDir . '/lib/public/AppFramework/OCS/OCSForbiddenException.php',
'OCP\\AppFramework\\OCS\\OCSNotFoundException' => $baseDir . '/lib/public/AppFramework/OCS/OCSNotFoundException.php',
'OCP\\AppFramework\\PublicShareController' => $baseDir . '/lib/public/AppFramework/PublicShareController.php',
'OCP\\AppFramework\\QueryException' => $baseDir . '/lib/public/AppFramework/QueryException.php',
'OCP\\AppFramework\\Utility\\IControllerMethodReflector' => $baseDir . '/lib/public/AppFramework/Utility/IControllerMethodReflector.php',
'OCP\\AppFramework\\Utility\\ITimeFactory' => $baseDir . '/lib/public/AppFramework/Utility/ITimeFactory.php',
@ -350,6 +352,8 @@ return array(
'OC\\AppFramework\\Http\\Request' => $baseDir . '/lib/private/AppFramework/Http/Request.php',
'OC\\AppFramework\\Middleware\\MiddlewareDispatcher' => $baseDir . '/lib/private/AppFramework/Middleware/MiddlewareDispatcher.php',
'OC\\AppFramework\\Middleware\\OCSMiddleware' => $baseDir . '/lib/private/AppFramework/Middleware/OCSMiddleware.php',
'OC\\AppFramework\\Middleware\\PublicShare\\Exceptions\\NeedAuthenticationException' => $baseDir . '/lib/private/AppFramework/Middleware/PublicShare/Exceptions/NeedAuthenticationException.php',
'OC\\AppFramework\\Middleware\\PublicShare\\PublicShareMiddleware' => $baseDir . '/lib/private/AppFramework/Middleware/PublicShare/PublicShareMiddleware.php',
'OC\\AppFramework\\Middleware\\Security\\BruteForceMiddleware' => $baseDir . '/lib/private/AppFramework/Middleware/Security/BruteForceMiddleware.php',
'OC\\AppFramework\\Middleware\\Security\\CORSMiddleware' => $baseDir . '/lib/private/AppFramework/Middleware/Security/CORSMiddleware.php',
'OC\\AppFramework\\Middleware\\Security\\Exceptions\\AppNotEnabledException' => $baseDir . '/lib/private/AppFramework/Middleware/Security/Exceptions/AppNotEnabledException.php',

@ -48,6 +48,7 @@ class ComposerStaticInit53792487c5a8370acc0b06b1a864ff4c
'OCP\\App' => __DIR__ . '/../../..' . '/lib/public/App.php',
'OCP\\AppFramework\\ApiController' => __DIR__ . '/../../..' . '/lib/public/AppFramework/ApiController.php',
'OCP\\AppFramework\\App' => __DIR__ . '/../../..' . '/lib/public/AppFramework/App.php',
'OCP\\AppFramework\\AuthPublicShareController' => __DIR__ . '/../../..' . '/lib/public/AppFramework/AuthPublicShareController.php',
'OCP\\AppFramework\\Controller' => __DIR__ . '/../../..' . '/lib/public/AppFramework/Controller.php',
'OCP\\AppFramework\\Db\\DoesNotExistException' => __DIR__ . '/../../..' . '/lib/public/AppFramework/Db/DoesNotExistException.php',
'OCP\\AppFramework\\Db\\Entity' => __DIR__ . '/../../..' . '/lib/public/AppFramework/Db/Entity.php',
@ -86,6 +87,7 @@ class ComposerStaticInit53792487c5a8370acc0b06b1a864ff4c
'OCP\\AppFramework\\OCS\\OCSException' => __DIR__ . '/../../..' . '/lib/public/AppFramework/OCS/OCSException.php',
'OCP\\AppFramework\\OCS\\OCSForbiddenException' => __DIR__ . '/../../..' . '/lib/public/AppFramework/OCS/OCSForbiddenException.php',
'OCP\\AppFramework\\OCS\\OCSNotFoundException' => __DIR__ . '/../../..' . '/lib/public/AppFramework/OCS/OCSNotFoundException.php',
'OCP\\AppFramework\\PublicShareController' => __DIR__ . '/../../..' . '/lib/public/AppFramework/PublicShareController.php',
'OCP\\AppFramework\\QueryException' => __DIR__ . '/../../..' . '/lib/public/AppFramework/QueryException.php',
'OCP\\AppFramework\\Utility\\IControllerMethodReflector' => __DIR__ . '/../../..' . '/lib/public/AppFramework/Utility/IControllerMethodReflector.php',
'OCP\\AppFramework\\Utility\\ITimeFactory' => __DIR__ . '/../../..' . '/lib/public/AppFramework/Utility/ITimeFactory.php',
@ -380,6 +382,8 @@ class ComposerStaticInit53792487c5a8370acc0b06b1a864ff4c
'OC\\AppFramework\\Http\\Request' => __DIR__ . '/../../..' . '/lib/private/AppFramework/Http/Request.php',
'OC\\AppFramework\\Middleware\\MiddlewareDispatcher' => __DIR__ . '/../../..' . '/lib/private/AppFramework/Middleware/MiddlewareDispatcher.php',
'OC\\AppFramework\\Middleware\\OCSMiddleware' => __DIR__ . '/../../..' . '/lib/private/AppFramework/Middleware/OCSMiddleware.php',
'OC\\AppFramework\\Middleware\\PublicShare\\Exceptions\\NeedAuthenticationException' => __DIR__ . '/../../..' . '/lib/private/AppFramework/Middleware/PublicShare/Exceptions/NeedAuthenticationException.php',
'OC\\AppFramework\\Middleware\\PublicShare\\PublicShareMiddleware' => __DIR__ . '/../../..' . '/lib/private/AppFramework/Middleware/PublicShare/PublicShareMiddleware.php',
'OC\\AppFramework\\Middleware\\Security\\BruteForceMiddleware' => __DIR__ . '/../../..' . '/lib/private/AppFramework/Middleware/Security/BruteForceMiddleware.php',
'OC\\AppFramework\\Middleware\\Security\\CORSMiddleware' => __DIR__ . '/../../..' . '/lib/private/AppFramework/Middleware/Security/CORSMiddleware.php',
'OC\\AppFramework\\Middleware\\Security\\Exceptions\\AppNotEnabledException' => __DIR__ . '/../../..' . '/lib/private/AppFramework/Middleware/Security/Exceptions/AppNotEnabledException.php',

@ -62,6 +62,7 @@ use OCP\IL10N;
use OCP\ILogger;
use OCP\IRequest;
use OCP\IServerContainer;
use OCP\ISession;
use OCP\IUserSession;
use OCP\RichObjectStrings\IValidator;
use OCP\Encryption\IManager;
@ -304,7 +305,7 @@ class DIContainer extends SimpleContainer implements IAppContainer {
});
$middleWares = &$this->middleWares;
$this->registerService('MiddlewareDispatcher', function($c) use (&$middleWares) {
$this->registerService('MiddlewareDispatcher', function(SimpleContainer $c) use (&$middleWares) {
$dispatcher = new MiddlewareDispatcher();
$dispatcher->registerMiddleware($c[OC\AppFramework\Middleware\Security\SameSiteCookieMiddleware::class]);
$dispatcher->registerMiddleware($c['CORSMiddleware']);
@ -314,6 +315,11 @@ class DIContainer extends SimpleContainer implements IAppContainer {
$dispatcher->registerMiddleware($c['TwoFactorMiddleware']);
$dispatcher->registerMiddleware($c['BruteForceMiddleware']);
$dispatcher->registerMiddleware($c['RateLimitingMiddleware']);
$dispatcher->registerMiddleware(new OC\AppFramework\Middleware\PublicShare\PublicShareMiddleware(
$c['Request'],
$c->query(ISession::class),
$c->query(\OCP\IConfig::class)
));
foreach($middleWares as $middleWare) {
$dispatcher->registerMiddleware($c[$middleWare]);

@ -0,0 +1,7 @@
<?php
namespace OC\AppFramework\Middleware\PublicShare\Exceptions;
class NeedAuthenticationException extends \Exception {
}

@ -0,0 +1,112 @@
<?php
namespace OC\AppFramework\Middleware\PublicShare;
use OC\AppFramework\Middleware\PublicShare\Exceptions\NeedAuthenticationException;
use OCP\AppFramework\AuthPublicShareController;
use OCP\AppFramework\Http\NotFoundResponse;
use OCP\AppFramework\Http\Response;
use OCP\AppFramework\Middleware;
use OCP\AppFramework\PublicShareController;
use OCP\Files\NotFoundException;
use OCP\IConfig;
use OCP\IRequest;
use OCP\ISession;
class PublicShareMiddleware extends Middleware {
/** @var IRequest */
private $request;
/** @var ISession */
private $session;
/** @var IConfig */
private $config;
public function __construct(IRequest $request, ISession $session, IConfig $config) {
$this->request = $request;
$this->session = $session;
$this->config = $config;
}
public function beforeController($controller, $methodName) {
if (!($controller instanceof PublicShareController)) {
return;
}
if (!$this->isLinkSharingEnabled()) {
throw new NotFoundException('Link sharing is disabled');
}
// We require the token parameter to be set
$token = $this->request->getParam('token');
if ($token === null) {
throw new NotFoundException();
}
// Set the token
$controller->setToken($token);
if (!$controller->isValidToken()) {
$controller->shareNotFound();
throw new NotFoundException();
}
// No need to check for authentication when we try to authenticate
if ($methodName === 'authenticate' || $methodName === 'showAuthenticate') {
return;
}
// If authentication succeeds just continue
if ($controller->isAuthenticated()) {
return;
}
// If we can authenticate to this controller do it else we throw a 404 to not leak any info
if ($controller instanceof AuthPublicShareController) {
$this->session->set('public_link_authenticate_redirect', json_encode($this->request->getParams()));
throw new NeedAuthenticationException();
}
throw new NotFoundException();
}
public function afterException($controller, $methodName, \Exception $exception) {
if (!($controller instanceof PublicShareController)) {
throw $exception;
}
if ($exception instanceof NotFoundException) {
return new NotFoundResponse();
}
if ($controller instanceof AuthPublicShareController && $exception instanceof NeedAuthenticationException) {
return $controller->getAuthenticationRedirect($this->getFunctionForRoute($this->request->getParam('_route')));
}
throw $exception;
}
private function getFunctionForRoute(string $route): string {
$tmp = explode('.', $route);
return array_pop($tmp);
}
/**
* Check if link sharing is allowed
*/
private function isLinkSharingEnabled(): bool {
// Check if the shareAPI is enabled
if ($this->config->getAppValue('core', 'shareapi_enabled', 'yes') !== 'yes') {
return false;
}
// Check whether public sharing is enabled
if($this->config->getAppValue('core', 'shareapi_allow_links', 'yes') !== 'yes') {
return false;
}
return true;
}
}

@ -262,7 +262,7 @@ function preview_icon( $path ) {
* @return string
*/
function publicPreview_icon ( $path, $token ) {
return \OC::$server->getURLGenerator()->linkToRoute('files_sharing.PublicPreview.getPreview', ['x' => 32, 'y' => 32, 'file' => $path, 't' => $token]);
return \OC::$server->getURLGenerator()->linkToRoute('files_sharing.PublicPreview.getPreview', ['x' => 32, 'y' => 32, 'file' => $path, 'token' => $token]);
}
/**

@ -0,0 +1,192 @@
<?php
/**
* @copyright 2018, Roeland Jago Douma <roeland@famdouma.nl>
*
* @author Roeland Jago Douma <roeland@famdouma.nl>
*
* @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 <http://www.gnu.org/licenses/>.
*
*/
declare(strict_types=1);
namespace OCP\AppFramework;
use OCP\AppFramework\Http\RedirectResponse;
use OCP\AppFramework\Http\TemplateResponse;
use OCP\IRequest;
use OCP\ISession;
use OCP\IURLGenerator;
/**
* Base controller for interactive public shares
*
* It will verify if the user is properly authenticated to the share. If not the
* user will be redirected to an authentication page.
*
* Use this for a controller that is to be called directly by a user. So the
* normal public share page for files/calendars etc.
*
* @since 14.0.0
*/
abstract class AuthPublicShareController extends PublicShareController {
/** @var IURLGenerator */
protected $urlGenerator;
/**
* @since 14.0.0
*/
public function __construct(string $appName,
IRequest $request,
ISession $session,
IURLGenerator $urlGenerator) {
parent::__construct($appName, $request, $session);
$this->urlGenerator = $urlGenerator;
}
/**
* @PublicPage
* @NoCSRFRequired
*
* Show the authentication page
* The form has to submit to the authenticate method route
*
* @since 14.0.0
*/
public function showAuthenticate(): TemplateResponse {
return new TemplateResponse('core', 'publicshareauth', [], 'guest');
}
/**
* The template to show when authentication failed
*
* @since 14.0.0
*/
protected function showAuthFailed(): TemplateResponse {
return new TemplateResponse('core', 'publicshareauth', ['wrongpw' => true], 'guest');
}
/**
* Verify the password
*
* @since 14.0.0
*/
abstract protected function verifyPassword(string $password): bool;
/**
* Function called after failed authentication
*
* You can use this to do some logging for example
*
* @since 14.0.0
*/
protected function authFailed() {
}
/**
* Function called after successfull authentication
*
* You can use this to do some logging for example
*
* @since 14.0.0
*/
protected function authSucceeded() {
}
/**
* @UseSession
* @PublicPage
* @BruteForceProtection(action=publicLinkAuth)
*
* Authenticate the share
*
* @since 14.0.0
*/
final public function authenticate(string $password = '') {
// Already authenticated
if ($this->isAuthenticated()) {
return $this->getRedirect();
}
if (!$this->verifyPassword($password)) {
$this->authFailed();
$response = $this->showAuthFailed();
$response->throttle();
return $response;
}
$this->session->regenerateId(true, true);
$response = $this->getRedirect();
$this->session->set('public_link_authenticated_token', $this->getToken());
$this->session->set('public_link_authenticated_password_hash', $this->getPasswordHash());
$this->authSucceeded();
return $response;
}
/**
* Default landing page
*
* @since 14.0.0
*/
abstract public function showShare(): TemplateResponse;
/**
* @since 14.0.0
*/
final public function getAuthenticationRedirect(string $redirect): RedirectResponse {
return new RedirectResponse(
$this->urlGenerator->linkToRoute($this->getRoute('showAuthenticate'), ['token' => $this->getToken(), 'redirect' => $redirect])
);
}
/**
* @since 14.0.0
*/
private function getRoute(string $function): string {
$app = strtolower($this->appName);
$class = strtolower((new \ReflectionClass($this))->getShortName());
return $app . '.' . $class . '.' . $function;
}
/**
* @since 14.0.0
*/
private function getRedirect(): RedirectResponse {
//Get all the stored redirect parameters:
$params = $this->session->get('public_link_authenticate_redirect');
$route = $this->getRoute('showShare');
if ($params === null) {
$params = [
'token' => $this->getToken(),
];
} else {
$params = json_decode($params, true);
if (isset($params['_route'])) {
$route = $params['_route'];
unset($params['_route']);
}
}
return new RedirectResponse($this->urlGenerator->linkToRoute($route, $params));
}
}

@ -0,0 +1,138 @@
<?php
/**
* @copyright 2018, Roeland Jago Douma <roeland@famdouma.nl>
*
* @author Roeland Jago Douma <roeland@famdouma.nl>
*
* @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 <http://www.gnu.org/licenses/>.
*
*/
declare(strict_types=1);
namespace OCP\AppFramework;
use OCP\IRequest;
use OCP\ISession;
/**
* Base controller for public shares
*
* It will verify if the user is properly authenticated to the share. If not a 404
* is thrown by the PublicShareMiddleware.
*
* Use this for example for a controller that is not to be called via a webbrowser
* directly. For example a PublicPreviewController. As this is not meant to be
* called by a user direclty.
*
* To show an auth page extend the AuthPublicShareController
*
* @since 14.0.0
*/
abstract class PublicShareController extends Controller {
/** @var ISession */
protected $session;
/** @var string */
private $token;
/**
* @since 14.0.0
*/
public function __construct(string $appName,
IRequest $request,
ISession $session) {
parent::__construct($appName, $request);
$this->session = $session;
}
/**
* Middleware set the token for the request
*
* @since 14.0.0
*/
final public function setToken(string $token) {
$this->token = $token;
}
/**
* Get the token for this request
*
* @since 14.0.0
*/
final public function getToken(): string {
return $this->token;
}
/**
* Get a hash of the password for this share
*
* To ensure access is blocked when the password to a share is changed we store
* a hash of the password for this token.
*
* @since 14.0.0
*/
abstract protected function getPasswordHash(): string;
/**
* Is the provided token a valid token
*
* This function is already called from the middleware directly after setting the token.
*
* @since 14.0.0
*/
abstract public function isValidToken(): bool;
/**
* Is a share with this token password protected
*
* @since 14.0.0
*/
abstract protected function isPasswordProtected(): bool;
/**
* Check if a share is authenticated or not
*
* @since 14.0.0
*/
final public function isAuthenticated(): bool {
// Always authenticated against non password protected shares
if (!$this->isPasswordProtected()) {
return true;
}
// If we are authenticated properly
if ($this->session->get('public_link_authenticated_token') === $this->getToken() &&
$this->session->get('public_link_authenticated_password_hash') === $this->getPasswordHash()) {
return true;
}
// Fail by default if nothing matches
return false;
}
/**
* Function called if the share is not found.
*
* You can use this to do some logging for example
*
* @since 14.0.0
*/
public function shareNotFound() {
}
}

@ -137,7 +137,7 @@ class FilesSharingAppContext implements Context, ActorAwareInterface {
*/
public function iSeeThatTheCurrentPageIsTheAuthenticatePageForTheSharedLinkIWroteDown() {
PHPUnit_Framework_Assert::assertEquals(
$this->actor->getSharedNotebook()["shared link"] . "/authenticate/preview",
$this->actor->getSharedNotebook()["shared link"] . "/authenticate/showShare",
$this->actor->getSession()->getCurrentUrl());
}
@ -146,7 +146,7 @@ class FilesSharingAppContext implements Context, ActorAwareInterface {
*/
public function iSeeThatTheCurrentPageIsTheAuthenticatePageForTheDirectDownloadSharedLinkIWroteDown() {
PHPUnit_Framework_Assert::assertEquals(
$this->actor->getSharedNotebook()["shared link"] . "/authenticate/download",
$this->actor->getSharedNotebook()["shared link"] . "/authenticate/downloadShare",
$this->actor->getSession()->getCurrentUrl());
}

@ -0,0 +1,159 @@
<?php
/**
* @copyright 2018, Roeland Jago Douma <roeland@famdouma.nl>
*
* @author Roeland Jago Douma <roeland@famdouma.nl>
*
* @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 <http://www.gnu.org/licenses/>.
*
*/
namespace Test\AppFramework\Controller;
use OC\AppFramework\Middleware\PublicShare\Exceptions\NeedAuthenticationException;
use OC\AppFramework\Middleware\PublicShare\PublicShareMiddleware;
use OCP\AppFramework\AuthPublicShareController;
use OCP\AppFramework\Controller;
use OCP\AppFramework\Http\NotFoundResponse;
use OCP\AppFramework\Http\RedirectResponse;
use OCP\AppFramework\Http\TemplateResponse;
use OCP\AppFramework\PublicShareController;
use OCP\Files\NotFoundException;
use OCP\IConfig;
use OCP\IRequest;
use OCP\ISession;
use OCP\IURLGenerator;
class AuthPublicShareControllerTest extends \Test\TestCase {
/** @var IRequest|\PHPUnit_Framework_MockObject_MockObject */
private $request;
/** @var ISession|\PHPUnit_Framework_MockObject_MockObject */
private $session;
/** @var IURLGenerator|\PHPUnit_Framework_MockObject_MockObject */
private $urlGenerator;
/** @var AuthPublicShareController|\PHPUnit_Framework_MockObject_MockObject */
private $controller;
protected function setUp() {
parent::setUp();
$this->request = $this->createMock(IRequest::class);
$this->session = $this->createMock(ISession::class);
$this->urlGenerator = $this->createMock(IURLGenerator::class);
$this->controller = $this->getMockBuilder(AuthPublicShareController::class)
->setConstructorArgs([
'app',
$this->request,
$this->session,
$this->urlGenerator
])->setMethods([
'authFailed',
'getPasswordHash',
'isAuthenticated',
'isPasswordProtected',
'isValidToken',
'showShare',
'verifyPassword'
])->getMock();
}
public function testShowAuthenticate() {
$expects = new TemplateResponse('core', 'publicshareauth', [], 'guest');
$this->assertEquals($expects, $this->controller->showAuthenticate());
}
public function testAuthenticateAuthenticated() {
$this->controller->method('isAuthenticated')
->willReturn(true);
$this->controller->setToken('myToken');
$this->session->method('get')
->willReturnMap(['public_link_authenticate_redirect', ['foo' => 'bar']]);
$this->urlGenerator->method('linkToRoute')
->willReturn('myLink!');
$result = $this->controller->authenticate('password');
$this->assertInstanceOf(RedirectResponse::class, $result);
$this->assertSame('myLink!', $result->getRedirectURL());
}
public function testAuthenticateInvalidPassword() {
$this->controller->setToken('token');
$this->controller->method('isPasswordProtected')
->willReturn(true);
$this->controller->method('verifyPassword')
->with('password')
->willReturn(false);
$this->controller->expects($this->once())
->method('authFailed');
$expects = new TemplateResponse('core', 'publicshareauth', ['wrongpw' => true], 'guest');
$expects->throttle();
$result = $this->controller->authenticate('password');
$this->assertEquals($expects, $result);
}
public function testAuthenticateValidPassword() {
$this->controller->setToken('token');
$this->controller->method('isPasswordProtected')
->willReturn(true);
$this->controller->method('verifyPassword')
->with('password')
->willReturn(true);
$this->controller->method('getPasswordHash')
->willReturn('hash');
$this->session->expects($this->once())
->method('regenerateId');
$this->session->method('get')
->willReturnMap(['public_link_authenticate_redirect', ['foo' => 'bar']]);
$tokenSet = false;
$hashSet = false;
$this->session
->method('set')
->will($this->returnCallback(function($key, $value) use (&$tokenSet, &$hashSet) {
if ($key === 'public_link_authenticated_token' && $value === 'token') {
$tokenSet = true;
return true;
}
if ($key === 'public_link_authenticated_password_hash' && $value === 'hash') {
$hashSet = true;
return true;
}
return false;
}));
$this->urlGenerator->method('linkToRoute')
->willReturn('myLink!');
$result = $this->controller->authenticate('password');
$this->assertInstanceOf(RedirectResponse::class, $result);
$this->assertSame('myLink!', $result->getRedirectURL());
$this->assertTrue($tokenSet);
$this->assertTrue($hashSet);
}
}

@ -0,0 +1,102 @@
<?php
/**
* @copyright 2018, Roeland Jago Douma <roeland@famdouma.nl>
*
* @author Roeland Jago Douma <roeland@famdouma.nl>
*
* @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 <http://www.gnu.org/licenses/>.
*
*/
namespace Test\AppFramework\Controller;
use OC\AppFramework\Middleware\PublicShare\Exceptions\NeedAuthenticationException;
use OC\AppFramework\Middleware\PublicShare\PublicShareMiddleware;
use OCP\AppFramework\AuthPublicShareController;
use OCP\AppFramework\Controller;
use OCP\AppFramework\Http\NotFoundResponse;
use OCP\AppFramework\Http\RedirectResponse;
use OCP\AppFramework\PublicShareController;
use OCP\Files\NotFoundException;
use OCP\IConfig;
use OCP\IRequest;
use OCP\ISession;
use OCP\IURLGenerator;
class PublicShareControllerTest extends \Test\TestCase {
/** @var IRequest|\PHPUnit_Framework_MockObject_MockObject */
private $request;
/** @var ISession|\PHPUnit_Framework_MockObject_MockObject */
private $session;
/** @var PublicShareController|\PHPUnit_Framework_MockObject_MockObject */
private $controller;
protected function setUp() {
parent::setUp();
$this->request = $this->createMock(IRequest::class);
$this->session = $this->createMock(ISession::class);
$this->controller = $this->getMockBuilder(PublicShareController::class)
->setConstructorArgs([
'app',
$this->request,
$this->session
])->getMock();
}
public function testGetToken() {
$this->controller->setToken('test');
$this->assertEquals('test', $this->controller->getToken());
}
public function dataIsAuthenticated() {
return [
[false, 'token1', 'token1', 'hash1', 'hash1', true],
[false, 'token1', 'token1', 'hash1', 'hash2', true],
[false, 'token1', 'token2', 'hash1', 'hash1', true],
[false, 'token1', 'token2', 'hash1', 'hash2', true],
[ true, 'token1', 'token1', 'hash1', 'hash1', true],
[ true, 'token1', 'token1', 'hash1', 'hash2', false],
[ true, 'token1', 'token2', 'hash1', 'hash1', false],
[ true, 'token1', 'token2', 'hash1', 'hash2', false],
];
}
/**
* @dataProvider dataIsAuthenticated
*/
public function testIsAuthenticatedNotPasswordProtected(bool $protected, string $token1, string $token2, string $hash1, string $hash2, bool $expected) {
$this->controller->method('isPasswordProtected')
->willReturn($protected);
$this->session->method('get')
->willReturnMap([
['public_link_authenticated_token', $token1],
['public_link_authenticated_password_hash', $hash1],
]);
$this->controller->setToken($token2);
$this->controller->method('getPasswordHash')
->willReturn($hash2);
$this->assertEquals($expected, $this->controller->isAuthenticated());
}
}

@ -0,0 +1,287 @@
<?php
/**
* @copyright 2018, Roeland Jago Douma <roeland@famdouma.nl>
*
* @author Roeland Jago Douma <roeland@famdouma.nl>
*
* @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 <http://www.gnu.org/licenses/>.
*
*/
namespace Test\AppFramework\Middleware\PublicShare;
use OC\AppFramework\Middleware\PublicShare\Exceptions\NeedAuthenticationException;
use OC\AppFramework\Middleware\PublicShare\PublicShareMiddleware;
use OCP\AppFramework\AuthPublicShareController;
use OCP\AppFramework\Controller;
use OCP\AppFramework\Http\NotFoundResponse;
use OCP\AppFramework\Http\RedirectResponse;
use OCP\AppFramework\PublicShareController;
use OCP\Files\NotFoundException;
use OCP\IConfig;
use OCP\IRequest;
use OCP\ISession;
use OCP\IURLGenerator;
class PublicShareMiddlewareTest extends \Test\TestCase {
/** @var IRequest|\PHPUnit_Framework_MockObject_MockObject */
private $request;
/** @var ISession|\PHPUnit_Framework_MockObject_MockObject */
private $session;
/** @var IConfig|\PHPUnit_Framework_MockObject_MockObject */
private $config;
/** @var PublicShareMiddleware */
private $middleware;
protected function setUp() {
parent::setUp();
$this->request = $this->createMock(IRequest::class);
$this->session = $this->createMock(ISession::class);
$this->config = $this->createMock(IConfig::class);
$this->middleware = new PublicShareMiddleware(
$this->request,
$this->session,
$this->config
);
}
public function testBeforeControllerNoPublicShareController() {
$controller = $this->createMock(Controller::class);
$this->middleware->beforeController($controller, 'method');
$this->assertTrue(true);
}
public function dataShareApi() {
return [
['no', 'no',],
['no', 'yes',],
['yes', 'no',],
];
}
/**
* @dataProvider dataShareApi
*/
public function testBeforeControllerShareApiDisabled(string $shareApi, string $shareLinks) {
$controller = $this->createMock(PublicShareController::class);
$this->config->method('getAppValue')
->willReturnMap([
['core', 'shareapi_enabled', 'yes', $shareApi],
['core', 'shareapi_allow_links', 'yes', $shareLinks],
]);
$this->expectException(NotFoundException::class);
$this->middleware->beforeController($controller, 'mehod');
}
public function testBeforeControllerNoTokenParam() {
$controller = $this->createMock(PublicShareController::class);
$this->config->method('getAppValue')
->willReturnMap([
['core', 'shareapi_enabled', 'yes', 'yes'],
['core', 'shareapi_allow_links', 'yes', 'yes'],
]);
$this->expectException(NotFoundException::class);
$this->middleware->beforeController($controller, 'mehod');
}
public function testBeforeControllerInvalidToken() {
$controller = $this->createMock(PublicShareController::class);
$this->config->method('getAppValue')
->willReturnMap([
['core', 'shareapi_enabled', 'yes', 'yes'],
['core', 'shareapi_allow_links', 'yes', 'yes'],
]);
$this->request->method('getParam')
->with('token', null)
->willReturn('myToken');
$controller->method('isValidToken')
->willReturn(false);
$controller->expects($this->once())
->method('shareNotFound');
$this->expectException(NotFoundException::class);
$this->middleware->beforeController($controller, 'mehod');
}
public function testBeforeControllerValidTokenNotAuthenticated() {
$controller = $this->getMockBuilder(PublicShareController::class)
->setConstructorArgs(['app', $this->request, $this->session])
->getMock();
$this->config->method('getAppValue')
->willReturnMap([
['core', 'shareapi_enabled', 'yes', 'yes'],
['core', 'shareapi_allow_links', 'yes', 'yes'],
]);
$this->request->method('getParam')
->with('token', null)
->willReturn('myToken');
$controller->method('isValidToken')
->willReturn(true);
$controller->method('isPasswordProtected')
->willReturn(true);
$this->expectException(NotFoundException::class);
$this->middleware->beforeController($controller, 'mehod');
}
public function testBeforeControllerValidTokenAuthenticateMethod() {
$controller = $this->getMockBuilder(PublicShareController::class)
->setConstructorArgs(['app', $this->request, $this->session])
->getMock();
$this->config->method('getAppValue')
->willReturnMap([
['core', 'shareapi_enabled', 'yes', 'yes'],
['core', 'shareapi_allow_links', 'yes', 'yes'],
]);
$this->request->method('getParam')
->with('token', null)
->willReturn('myToken');
$controller->method('isValidToken')
->willReturn(true);
$controller->method('isPasswordProtected')
->willReturn(true);
$this->middleware->beforeController($controller, 'authenticate');
$this->assertTrue(true);
}
public function testBeforeControllerValidTokenShowAuthenticateMethod() {
$controller = $this->getMockBuilder(PublicShareController::class)
->setConstructorArgs(['app', $this->request, $this->session])
->getMock();
$this->config->method('getAppValue')
->willReturnMap([
['core', 'shareapi_enabled', 'yes', 'yes'],
['core', 'shareapi_allow_links', 'yes', 'yes'],
]);
$this->request->method('getParam')
->with('token', null)
->willReturn('myToken');
$controller->method('isValidToken')
->willReturn(true);
$controller->method('isPasswordProtected')
->willReturn(true);
$this->middleware->beforeController($controller, 'showAuthenticate');
$this->assertTrue(true);
}
public function testBeforeControllerAuthPublicShareController() {
$controller = $this->getMockBuilder(AuthPublicShareController::class)
->setConstructorArgs(['app', $this->request, $this->session, $this->createMock(IURLGenerator::class)])
->getMock();
$this->config->method('getAppValue')
->willReturnMap([
['core', 'shareapi_enabled', 'yes', 'yes'],
['core', 'shareapi_allow_links', 'yes', 'yes'],
]);
$this->request->method('getParam')
->with('token', null)
->willReturn('myToken');
$controller->method('isValidToken')
->willReturn(true);
$controller->method('isPasswordProtected')
->willReturn(true);
$this->session->expects($this->once())
->method('set')
->with('public_link_authenticate_redirect', '[]');
$this->expectException(NeedAuthenticationException::class);
$this->middleware->beforeController($controller, 'method');
}
public function testAfterExceptionNoPublicShareController() {
$controller = $this->createMock(Controller::class);
$exception = new \Exception();
try {
$this->middleware->afterException($controller, 'method', $exception);
} catch (\Exception $e) {
$this->assertEquals($exception, $e);
}
}
public function testAfterExceptionPublicShareControllerNotFoundException() {
$controller = $this->createMock(PublicShareController::class);
$exception = new NotFoundException();
$result = $this->middleware->afterException($controller, 'method', $exception);
$this->assertInstanceOf(NotFoundResponse::class, $result);
}
public function testAfterExceptionPublicShareController() {
$controller = $this->createMock(PublicShareController::class);
$exception = new \Exception();
try {
$this->middleware->afterException($controller, 'method', $exception);
} catch (\Exception $e) {
$this->assertEquals($exception, $e);
}
}
public function testAfterExceptionAuthPublicShareController() {
$controller = $this->getMockBuilder(AuthPublicShareController::class)
->setConstructorArgs([
'app',
$this->request,
$this->session,
$this->createMock(IURLGenerator::class),
])->getMock();
$controller->setToken('token');
$exception = new NeedAuthenticationException();
$this->request->method('getParam')
->with('_route')
->willReturn('my.route');
$result = $this->middleware->afterException($controller, 'method', $exception);
$this->assertInstanceOf(RedirectResponse::class, $result);
}
}
Loading…
Cancel
Save