Make public pages work

Signed-off-by: Louis Chemineau <louis@chmn.me>
This commit is contained in:
Louis Chemineau 2022-09-28 23:20:53 +02:00
parent a2890b03e7
commit 142fe83638
29 changed files with 509 additions and 182 deletions

View File

@ -11,7 +11,8 @@
<namespace>Photos</namespace>
<category>multimedia</category>
<types>
<dav/>
<dav />
<authentication />
</types>
<website>https://github.com/nextcloud/photos</website>
@ -32,6 +33,7 @@
<sabre>
<collections>
<collection>OCA\Photos\Sabre\RootCollection</collection>
<collection>OCA\Photos\Sabre\PublicRootCollection</collection>
</collections>
<plugins>
<plugin>OCA\Photos\Sabre\Album\PropFindPlugin</plugin>

View File

@ -46,10 +46,7 @@ return [
'path' => '',
]
],
[ 'name' => 'publicAlbum#get', 'url' => '/public/{ownerId}/{token}', 'verb' => 'GET',
'requirements' => [
'ownerId' => '.*',
],
[ 'name' => 'publicAlbum#get', 'url' => '/public/{token}', 'verb' => 'GET',
'requirements' => [
'token' => '.*',
],
@ -132,5 +129,14 @@ return [
'fileId' => '.*',
]
],
[
'name' => 'publicPreview#index',
'url' => '/api/v1/publicPreview/{fileId}',
'verb' => 'GET',
'requirements' => [
'fileId' => '.*',
]
],
]
];

View File

@ -362,7 +362,7 @@ class AlbumMapper {
}
break;
case self::TYPE_LINK:
$collaborator['id'] = $this->random->generate(15, ISecureRandom::CHAR_UPPER . ISecureRandom::CHAR_LOWER . ISecureRandom::CHAR_DIGITS);
$collaborator['id'] = $this->random->generate(32, ISecureRandom::CHAR_UPPER . ISecureRandom::CHAR_LOWER . ISecureRandom::CHAR_DIGITS);
break;
default:
throw new \Exception('Invalid collaborator type: ' . $collaborator['type']);
@ -420,7 +420,13 @@ class AlbumMapper {
}
if (!isset($albumsById[$albumId])) {
$albumsById[$albumId] = new AlbumInfo($albumId, $row['album_user'], $row['album_name'].' ('.$row['album_user'].')', $row['location'], (int)$row['created'], (int)$row['last_added_photo']);
$albumName = $row['album_name'];
// Suffix album name with the album owner to prevent duplicates.
// Not done for public link as it would like owner's uid.
if ($collaboratorType !== self::TYPE_LINK) {
$row['album_name'].' ('.$row['album_user'].')';
}
$albumsById[$albumId] = new AlbumInfo($albumId, $row['album_user'], $albumName, $row['location'], (int)$row['created'], (int)$row['last_added_photo']);
}
}
@ -452,7 +458,7 @@ class AlbumMapper {
* @param int $fileId
* @return AlbumInfo[]
*/
public function getAlbumForCollaboratorIdAndFileId(string $collaboratorId, int $collaboratorType, int $fileId): array {
public function getAlbumsForCollaboratorIdAndFileId(string $collaboratorId, int $collaboratorType, int $fileId): array {
$query = $this->connection->getQueryBuilder();
$rows = $query
->select("a.album_id", "name", "user", "location", "created", "last_added_photo")

View File

@ -25,6 +25,8 @@ declare(strict_types=1);
namespace OCA\Photos\AppInfo;
use OCA\DAV\Events\SabrePluginAuthInitEvent;
use OCA\Photos\Listener\SabrePluginAuthInitListener;
use OCA\DAV\Connector\Sabre\Principal;
use OCA\Photos\Listener\CacheEntryRemovedListener;
use OCP\AppFramework\App;
@ -64,6 +66,7 @@ class Application extends App implements IBootstrap {
/** Register $principalBackend for the DAV collection */
$context->registerServiceAlias('principalBackend', Principal::class);
$context->registerEventListener(CacheEntryRemovedEvent::class, CacheEntryRemovedListener::class);
$context->registerEventListener(SabrePluginAuthInitEvent::class, SabrePluginAuthInitListener::class);
}
public function boot(IBootContext $context): void {

View File

@ -43,16 +43,16 @@ use OCP\IUserSession;
class PreviewController extends Controller {
private IUserSession $userSession;
private Folder $userFolder;
private ?Folder $userFolder;
private IRootFolder $rootFolder;
private AlbumMapper $albumMapper;
protected AlbumMapper $albumMapper;
private IPreview $preview;
private IGroupManager $groupManager;
public function __construct(
IRequest $request,
IUserSession $userSession,
Folder $userFolder,
?Folder $userFolder,
IRootFolder $rootFolder,
AlbumMapper $albumMapper,
IPreview $preview,
@ -67,7 +67,6 @@ class PreviewController extends Controller {
$this->preview = $preview;
$this->groupManager = $groupManager;
}
/**
* @NoAdminRequired
* @NoCSRFRequired
@ -85,25 +84,35 @@ class PreviewController extends Controller {
}
$user = $this->userSession->getUser();
$nodes = $this->userFolder->getById($fileId);
if (\count($nodes) === 0) {
$albums = $this->albumMapper->getForUserAndFile($user->getUID(), $fileId);
$receivedAlbums = $this->albumMapper->getAlbumForCollaboratorIdAndFileId($user->getUID(), AlbumMapper::TYPE_USER, $fileId);
$albums = array_merge($albums, $receivedAlbums);
$userGroups = $this->groupManager->getUserGroupIds($user);
foreach ($userGroups as $groupId) {
$albumsForGroup = $this->albumMapper->getAlbumForCollaboratorIdAndFileId($groupId, AlbumMapper::TYPE_GROUP, $fileId);
$albumsForGroup = array_udiff($albumsForGroup, $albums, fn ($a, $b) => $a->getId() - $b->getId());
$albums = array_merge($albums, $albumsForGroup);
if ($user === null || $this->userFolder === null) {
return new DataResponse([], Http::STATUS_FORBIDDEN);
}
foreach ($albums as $album) {
$albumFile = $this->albumMapper->getForAlbumIdAndFileId($album->getId(), $fileId);
$nodes = $this->rootFolder
->getUserFolder($albumFile->getOwner())
->getById($fileId);
$nodes = $this->userFolder->getById($fileId);
/** @var \OCA\Photos\Album\AlbumInfo[] */
$checkedAlbums = [];
if (\count($nodes) === 0) {
$albumsOfCurrentUser = $this->albumMapper->getForUserAndFile($user->getUID(), $fileId);
$nodes = $this->getFileIdForAlbums($fileId, $albumsOfCurrentUser);
$checkedAlbums = $albumsOfCurrentUser;
}
if (\count($nodes) === 0) {
$receivedAlbums = $this->albumMapper->getAlbumsForCollaboratorIdAndFileId($user->getUID(), AlbumMapper::TYPE_USER, $fileId);
$receivedAlbums = array_udiff($checkedAlbums, $receivedAlbums, fn ($a, $b) => strcmp($a->getId(), $b->getId()));
$nodes = $this->getFileIdForAlbums($fileId, $receivedAlbums);
$checkedAlbums = array_merge($checkedAlbums, $receivedAlbums);
}
if (\count($nodes) === 0) {
$userGroups = $this->groupManager->getUserGroupIds($user);
foreach ($userGroups as $groupId) {
$albumsForGroup = $this->albumMapper->getAlbumsForCollaboratorIdAndFileId($groupId, AlbumMapper::TYPE_GROUP, $fileId);
$albumsForGroup = array_udiff($checkedAlbums, $albumsForGroup, fn ($a, $b) => strcmp($a->getId(), $b->getId()));
$nodes = $this->getFileIdForAlbums($fileId, $albumsForGroup);
$checkedAlbums = array_merge($checkedAlbums, $receivedAlbums);
if (\count($nodes) !== 0) {
break;
}
@ -119,10 +128,25 @@ class PreviewController extends Controller {
return $this->fetchPreview($node, $x, $y);
}
protected function getFileIdForAlbums($fileId, $albums) {
foreach ($albums as $album) {
$albumFile = $this->albumMapper->getForAlbumIdAndFileId($album->getId(), $fileId);
$nodes = $this->rootFolder
->getUserFolder($albumFile->getOwner())
->getById($fileId);
if (\count($nodes) !== 0) {
return $nodes;
}
}
return [];
}
/**
* @return DataResponse|FileDisplayResponse
*/
private function fetchPreview(
protected function fetchPreview(
Node $node,
int $x,
int $y

View File

@ -25,36 +25,27 @@
namespace OCA\Photos\Controller;
use OCP\AppFramework\Controller;
use OCA\Files\Event\LoadSidebar;
use OCA\Photos\AppInfo\Application;
use OCA\Photos\Service\UserConfigService;
use OCA\Viewer\Event\LoadViewer;
use OCP\App\IAppManager;
use OCP\AppFramework\Http\ContentSecurityPolicy;
use OCP\AppFramework\Http\TemplateResponse;
use OCP\AppFramework\Http\Template\PublicTemplateResponse;
use OCP\EventDispatcher\IEventDispatcher;
use OCP\AppFramework\Services\IInitialState;
use OCP\IRequest;
use OCP\Util;
class PublicAlbumController extends Controller {
private IAppManager $appManager;
private IEventDispatcher $eventDispatcher;
private UserConfigService $userConfig;
private IInitialState $initialState;
public function __construct(
IRequest $request,
IAppManager $appManager,
IEventDispatcher $eventDispatcher,
UserConfigService $userConfig,
IInitialState $initialState,
IInitialState $initialState
) {
parent::__construct(Application::APP_ID, $request);
$this->appManager = $appManager;
$this->eventDispatcher = $eventDispatcher;
$this->userConfig = $userConfig;
$this->initialState = $initialState;
}
@ -62,7 +53,7 @@ class PublicAlbumController extends Controller {
* @PublicPage
* @NoCSRFRequired
*/
public function get(): TemplateResponse {
public function get(): PublicTemplateResponse {
$this->eventDispatcher->dispatch(LoadViewer::class, new LoadViewer());
$this->initialState->provideInitialState('image-mimes', Application::IMAGE_MIMES);
@ -74,7 +65,7 @@ class PublicAlbumController extends Controller {
Util::addScript(Application::APP_ID, 'photos-public');
Util::addStyle(Application::APP_ID, 'icons');
$response = new TemplateResponse(Application::APP_ID, 'main');
$response = new PublicTemplateResponse(Application::APP_ID, 'public');
$policy = new ContentSecurityPolicy();
$policy->addAllowedWorkerSrcDomain("'self'");

View File

@ -0,0 +1,67 @@
<?php
declare(strict_types=1);
/**
* @copyright Copyright (c) 2022, Louis Chmn <louis@chmn.me>
*
* @author Louis Chmn <louis@chmn.me>
*
* @license AGPL-3.0-or-later
*
* 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 OCA\Photos\Controller;
use OCA\Photos\Album\AlbumMapper;
use OCP\AppFramework\Http;
use OCP\AppFramework\Http\DataResponse;
use OCP\AppFramework\Http\FileDisplayResponse;
class PublicPreviewController extends PreviewController {
/**
* @NoAdminRequired
* @NoCSRFRequired
* @PublicPage
* Render default index template
*
* @return DataResponse|FileDisplayResponse
*/
public function index(
int $fileId = -1,
int $x = 32,
int $y = 32,
string $token = null
) {
if ($fileId === -1 || $x === 0 || $y === 0) {
return new DataResponse([], Http::STATUS_BAD_REQUEST);
}
if ($token === null) {
return new DataResponse([], Http::STATUS_FORBIDDEN);
}
$publicAlbums = $this->albumMapper->getAlbumsForCollaboratorIdAndFileId($token, AlbumMapper::TYPE_LINK, $fileId);
$nodes = $this->getFileIdForAlbums($fileId, $publicAlbums);
if (\count($nodes) === 0) {
return new DataResponse([], Http::STATUS_NOT_FOUND);
}
$node = array_pop($nodes);
return $this->fetchPreview($node, $x, $y);
}
}

View File

@ -0,0 +1,49 @@
<?php
declare(strict_types=1);
/**
* @copyright Copyright (c) 2022, Louis Chmn <louis@chmn.me>
*
* @author Louis Chmn <louis@chmn.me>
*
* @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 OCA\Photos\Listener;
use OCA\DAV\Events\SabrePluginAuthInitEvent;
use OCA\Photos\Sabre\PublicAlbumAuthBackend;
use OCP\EventDispatcher\Event;
use OCP\EventDispatcher\IEventListener;
class SabrePluginAuthInitListener implements IEventListener {
private PublicAlbumAuthBackend $publicAlbumAuthBackend;
public function __construct(PublicAlbumAuthBackend $publicAlbumAuthBackend) {
$this->publicAlbumAuthBackend = $publicAlbumAuthBackend;
}
public function handle(Event $event): void {
if (!($event instanceof SabrePluginAuthInitEvent)) {
return;
}
$server = $event->getServer();
$authPlugin = $server->getPlugin('auth');
$authPlugin->addBackend($this->publicAlbumAuthBackend);
}
}

View File

@ -137,6 +137,9 @@ class AlbumPhoto implements IFile {
public function isFavorite(): bool {
$tagManager = \OCP\Server::get(\OCP\ITagManager::class);
$tagger = $tagManager->load('files');
if ($tagger === null) {
return false;
}
$tags = $tagger->getTagsForObjects([$this->getFileId()]);
if ($tags === false || empty($tags)) {

View File

@ -31,7 +31,6 @@ use OCA\Photos\Service\UserConfigService;
use OCP\Files\Folder;
use OCP\Files\IRootFolder;
use OCP\Files\NotFoundException;
use OCP\IUser;
use Sabre\DAV\Exception\Conflict;
use Sabre\DAV\Exception\Forbidden;
use Sabre\DAV\Exception\NotFound;
@ -43,22 +42,19 @@ class AlbumRoot implements ICollection, ICopyTarget {
protected AlbumMapper $albumMapper;
protected AlbumWithFiles $album;
protected IRootFolder $rootFolder;
protected Folder $userFolder;
protected IUser $user;
protected string $userId;
public function __construct(
AlbumMapper $albumMapper,
AlbumWithFiles $album,
IRootFolder $rootFolder,
Folder $userFolder,
IUser $user,
string $userId,
UserConfigService $userConfigService
) {
$this->albumMapper = $albumMapper;
$this->album = $album;
$this->rootFolder = $rootFolder;
$this->userFolder = $userFolder;
$this->user = $user;
$this->userId = $userId;
$this->userConfigService = $userConfigService;
}
@ -82,7 +78,7 @@ class AlbumRoot implements ICollection, ICopyTarget {
protected function getPhotosLocationInfo() {
$photosLocation = $this->userConfigService->getUserConfig('photosLocation');
$userFolder = $this->rootFolder->getUserFolder($this->user->getUID());
$userFolder = $this->rootFolder->getUserFolder($this->userId);
return [$photosLocation, $userFolder];
}
@ -103,7 +99,7 @@ class AlbumRoot implements ICollection, ICopyTarget {
return $userFolder->newFolder($photosLocation);
}
$photosFolder = $this->userFolder->get($photosLocation);
$photosFolder = $userFolder->get($photosLocation);
if (!($photosFolder instanceof Folder)) {
throw new Conflict('The destination exists and is not a folder');
@ -159,23 +155,22 @@ class AlbumRoot implements ICollection, ICopyTarget {
}
public function copyInto($targetName, $sourcePath, INode $sourceNode): bool {
$uid = $this->user->getUID();
if ($sourceNode instanceof File) {
$sourceId = $sourceNode->getId();
$ownerUID = $sourceNode->getFileInfo()->getOwner()->getUID();
return $this->addFile($sourceId, $ownerUID);
}
$uid = $this->userId;
throw new \Exception("Can't add file to album, only files from $uid can be added");
}
protected function addFile(int $sourceId, string $ownerUID): bool {
$uid = $this->user->getUID();
if (in_array($sourceId, $this->album->getFileIds())) {
throw new Conflict("File $sourceId is already in the folder");
}
if ($ownerUID === $uid) {
if ($ownerUID === $this->userId) {
$this->albumMapper->addFile($this->album->getAlbum()->getId(), $sourceId, $ownerUID);
$node = current($this->userFolder->getById($sourceId));
$node = current($this->rootFolder->getUserFolder($ownerUID)->getById($sourceId));
$this->album->addFile(new AlbumFile($sourceId, $node->getName(), $node->getMimetype(), $node->getSize(), $node->getMTime(), $node->getEtag(), $node->getCreationTime(), $ownerUID));
return true;
}

View File

@ -27,9 +27,7 @@ use OCA\Photos\Album\AlbumInfo;
use OCA\Photos\Album\AlbumMapper;
use OCA\Photos\Album\AlbumWithFiles;
use OCA\Photos\Service\UserConfigService;
use OCP\Files\Folder;
use OCP\Files\IRootFolder;
use OCP\IUser;
use Sabre\DAV\Exception\Forbidden;
use Sabre\DAV\Exception\NotFound;
use Sabre\DAV\ICollection;
@ -37,9 +35,8 @@ use Sabre\DAV\ICollection;
class AlbumsHome implements ICollection {
protected AlbumMapper $albumMapper;
protected array $principalInfo;
protected IUser $user;
protected string $userId;
protected IRootFolder $rootFolder;
protected Folder $userFolder;
protected UserConfigService $userConfigService;
public const NAME = 'albums';
@ -52,15 +49,14 @@ class AlbumsHome implements ICollection {
public function __construct(
array $principalInfo,
AlbumMapper $albumMapper,
IUser $user,
string $userId,
IRootFolder $rootFolder,
UserConfigService $userConfigService
) {
$this->principalInfo = $principalInfo;
$this->albumMapper = $albumMapper;
$this->user = $user;
$this->userId = $userId;
$this->rootFolder = $rootFolder;
$this->userFolder = $rootFolder->getUserFolder($user->getUID());
$this->userConfigService = $userConfigService;
}
@ -90,8 +86,7 @@ class AlbumsHome implements ICollection {
* @return void
*/
public function createDirectory($name) {
$uid = $this->user->getUID();
$this->albumMapper->create($uid, $name);
$this->albumMapper->create($this->userId, $name);
}
public function getChild($name) {
@ -109,9 +104,9 @@ class AlbumsHome implements ICollection {
*/
public function getChildren(): array {
if ($this->children === null) {
$albumInfos = $this->albumMapper->getForUser($this->user->getUID());
$albumInfos = $this->albumMapper->getForUser($this->userId);
$this->children = array_map(function (AlbumInfo $albumInfo) {
return new AlbumRoot($this->albumMapper, new AlbumWithFiles($albumInfo, $this->albumMapper), $this->rootFolder, $this->userFolder, $this->user, $this->userConfigService);
return new AlbumRoot($this->albumMapper, new AlbumWithFiles($albumInfo, $this->albumMapper), $this->rootFolder, $this->userId, $this->userConfigService);
}, $albumInfos);
}

View File

@ -28,13 +28,13 @@ use OCA\DAV\Connector\Sabre\FilesPlugin;
use OCA\Photos\Album\AlbumMapper;
use OCP\IConfig;
use OCP\IPreview;
use OCP\Files\NotFoundException;
use Sabre\DAV\INode;
use Sabre\DAV\PropFind;
use Sabre\DAV\PropPatch;
use Sabre\DAV\Server;
use Sabre\DAV\ServerPlugin;
use Sabre\DAV\Tree;
use OCP\Files\NotFoundException;
class PropFindPlugin extends ServerPlugin {
public const ORIGINAL_NAME_PROPERTYNAME = '{http://nextcloud.org/ns}original-name';
@ -68,6 +68,19 @@ class PropFindPlugin extends ServerPlugin {
$this->metadataEnabled = $this->config->getSystemValueBool('enable_file_metadata', true);
}
/**
* Returns a plugin name.
*
* Using this name other plugins will be able to access other plugins
* using DAV\Server::getPlugin
*
* @return string
*/
public function getPluginName() {
return 'photosDavPlugin';
}
/**
* @return void
*/

View File

@ -24,8 +24,6 @@ declare(strict_types=1);
namespace OCA\Photos\Sabre\Album;
use Sabre\DAV\Exception\Forbidden;
use Sabre\DAV\Exception\Conflict;
use OCP\Files\Folder;
use Sabre\DAV\INode;
class PublicAlbumRoot extends AlbumRoot {

View File

@ -1,51 +0,0 @@
<?php
declare(strict_types=1);
/**
* @copyright Copyright (c) 2022 Robin Appelman <robin@icewind.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 OCA\Photos\Sabre\Album;
use Sabre\DAV\Exception\Forbidden;
use Sabre\DAV\Exception\NotFound;
use OCA\Photos\Album\AlbumMapper;
class PublicAlbumsHome extends AlbumsHome {
public const NAME = 'public';
/**
* @return never
*/
public function createDirectory($name) {
throw new Forbidden('Not allowed to create folders in this folder');
}
public function getChild($name) {
$albums = $this->albumMapper->getSharedAlbumsForCollaboratorWithFiles($name, AlbumMapper::TYPE_LINK);
$albums = array_filter($albums, fn ($album) => $album->getAlbum()->getUserId() === $this->user->getUid());
if (count($albums) !== 1) {
throw new NotFound();
}
return new PublicAlbumRoot($this->albumMapper, $albums[0], $this->rootFolder, $this->userFolder, $this->user, $this->userConfigService);
}
}

View File

@ -32,7 +32,7 @@ class SharedAlbumRoot extends AlbumRoot {
* @return void
*/
public function delete() {
$this->albumMapper->deleteUserFromAlbumCollaboratorsList($this->user->getUID(), $this->album->getAlbum()->getId());
$this->albumMapper->deleteUserFromAlbumCollaboratorsList($this->userId, $this->album->getAlbum()->getId());
}
/**
@ -51,8 +51,7 @@ class SharedAlbumRoot extends AlbumRoot {
fn ($collaborator) => $collaborator['type'].':'.$collaborator['id'],
$this->albumMapper->getCollaborators($this->album->getAlbum()->getId()),
);
$uid = $this->user->getUID();
if (!in_array(AlbumMapper::TYPE_USER.':'.$uid, $collaboratorIds)) {
if (!in_array(AlbumMapper::TYPE_USER.':'.$this->userId, $collaboratorIds)) {
return false;
}

View File

@ -26,12 +26,13 @@ namespace OCA\Photos\Sabre\Album;
use OCA\Photos\Album\AlbumWithFiles;
use OCA\Photos\Service\UserConfigService;
use Sabre\DAV\Exception\Forbidden;
use OCP\IUserManager;
use OCP\IGroupManager;
use OCA\Photos\Album\AlbumMapper;
use OCP\Files\IRootFolder;
use OCP\IUser;
class SharedAlbumsHome extends AlbumsHome {
private IUserManager $userManager;
private IGroupManager $groupManager;
public const NAME = 'sharedalbums';
@ -39,19 +40,21 @@ class SharedAlbumsHome extends AlbumsHome {
public function __construct(
array $principalInfo,
AlbumMapper $albumMapper,
IUser $user,
string $userId,
IRootFolder $rootFolder,
IUserManager $userManager,
IGroupManager $groupManager,
UserConfigService $userConfigService
) {
parent::__construct(
$principalInfo,
$albumMapper,
$user,
$userId,
$rootFolder,
$userConfigService
);
$this->userManager = $userManager;
$this->groupManager = $groupManager;
}
@ -67,17 +70,18 @@ class SharedAlbumsHome extends AlbumsHome {
*/
public function getChildren(): array {
if ($this->children === null) {
$albums = $this->albumMapper->getSharedAlbumsForCollaboratorWithFiles($this->user->getUID(), AlbumMapper::TYPE_USER);
$albums = $this->albumMapper->getSharedAlbumsForCollaboratorWithFiles($this->userId, AlbumMapper::TYPE_USER);
$userGroups = $this->groupManager->getUserGroupIds($this->user);
$user = $this->userManager->get($this->userId);
$userGroups = $this->groupManager->getUserGroupIds($user);
foreach ($userGroups as $groupId) {
$albumsForGroup = $this->albumMapper->getSharedAlbumsForCollaboratorWithFiles($groupId, AlbumMapper::TYPE_GROUP);
$albumsForGroup = array_udiff($albumsForGroup, $albums, fn ($a, $b) => $a->getAlbum()->getId() - $b->getAlbum()->getId());
$albums = array_merge($albums, $albumsForGroup);
}
$this->children = array_map(function (AlbumWithFiles $folder) {
return new SharedAlbumRoot($this->albumMapper, $folder, $this->rootFolder, $this->userFolder, $this->user, $this->userConfigService);
$this->children = array_map(function (AlbumWithFiles $album) {
return new SharedAlbumRoot($this->albumMapper, $album, $this->rootFolder, $this->userId, $this->userConfigService);
}, $albums);
}

View File

@ -26,10 +26,9 @@ namespace OCA\Photos\Sabre;
use OCA\Photos\Album\AlbumMapper;
use OCA\Photos\Sabre\Album\AlbumsHome;
use OCA\Photos\Sabre\Album\SharedAlbumsHome;
use OCA\Photos\Sabre\Album\PublicAlbumsHome;
use OCA\Photos\Service\UserConfigService;
use OCP\Files\IRootFolder;
use OCP\IUser;
use OCP\IUserManager;
use OCP\IGroupManager;
use Sabre\DAV\Exception\Forbidden;
use Sabre\DAV\Exception\NotFound;
@ -38,23 +37,26 @@ use Sabre\DAV\ICollection;
class PhotosHome implements ICollection {
private AlbumMapper $albumMapper;
private array $principalInfo;
private IUser $user;
private string $userId;
private IRootFolder $rootFolder;
private IUserManager $userManager;
private IGroupManager $groupManager;
private UserConfigService $userConfigService;
public function __construct(
array $principalInfo,
AlbumMapper $albumMapper,
IUser $user,
string $userId,
IRootFolder $rootFolder,
IUserManager $userManager,
IGroupManager $groupManager,
UserConfigService $userConfigService
) {
$this->principalInfo = $principalInfo;
$this->albumMapper = $albumMapper;
$this->user = $user;
$this->userId = $userId;
$this->rootFolder = $rootFolder;
$this->userManager = $userManager;
$this->groupManager = $groupManager;
$this->userConfigService = $userConfigService;
}
@ -92,11 +94,9 @@ class PhotosHome implements ICollection {
public function getChild($name) {
switch ($name) {
case AlbumsHome::NAME:
return new AlbumsHome($this->principalInfo, $this->albumMapper, $this->user, $this->rootFolder, $this->userConfigService);
return new AlbumsHome($this->principalInfo, $this->albumMapper, $this->userId, $this->rootFolder, $this->userConfigService);
case SharedAlbumsHome::NAME:
return new SharedAlbumsHome($this->principalInfo, $this->albumMapper, $this->user, $this->rootFolder, $this->groupManager, $this->userConfigService);
case PublicAlbumsHome::NAME:
return new PublicAlbumsHome($this->principalInfo, $this->albumMapper, $this->user, $this->rootFolder, $this->userConfigService);
return new SharedAlbumsHome($this->principalInfo, $this->albumMapper, $this->userId, $this->rootFolder, $this->userManager, $this->groupManager, $this->userConfigService);
}
throw new NotFound();
@ -107,14 +107,13 @@ class PhotosHome implements ICollection {
*/
public function getChildren(): array {
return [
new AlbumsHome($this->principalInfo, $this->albumMapper, $this->user, $this->rootFolder, $this->userConfigService),
new SharedAlbumsHome($this->principalInfo, $this->albumMapper, $this->user, $this->rootFolder, $this->groupManager, $this->userConfigService),
new PublicAlbumsHome($this->principalInfo, $this->albumMapper, $this->user, $this->rootFolder, $this->userConfigService),
new AlbumsHome($this->principalInfo, $this->albumMapper, $this->userId, $this->rootFolder, $this->userConfigService),
new SharedAlbumsHome($this->principalInfo, $this->albumMapper, $this->userId, $this->rootFolder, $this->userManager, $this->groupManager, $this->userConfigService),
];
}
public function childExists($name): bool {
return $name === AlbumsHome::NAME || $name === SharedAlbumsHome::NAME || $name === PublicAlbumsHome::NAME;
return $name === AlbumsHome::NAME || $name === SharedAlbumsHome::NAME;
}
public function getLastModified(): int {

View File

@ -0,0 +1,80 @@
<?php
/**
* @copyright Copyright (c) 2022 Louis Chmn <louis@chmn.me>
*
* @author Louis Chmn <louis@chmn.me>
*
* @license AGPL-3.0
*
* This code is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License, version 3,
* as published by the Free Software Foundation.
*
* 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, version 3,
* along with this program. If not, see <http://www.gnu.org/licenses/>
*
*/
namespace OCA\Photos\Sabre;
use OC\Security\Bruteforce\Throttler;
use OCA\Photos\Album\AlbumMapper;
use OCA\Photos\Album\AlbumWithFiles;
use OCP\IRequest;
use Sabre\DAV\Auth\Backend\AbstractBasic;
class PublicAlbumAuthBackend extends AbstractBasic {
private const BRUTEFORCE_ACTION = 'public_webdav_auth';
private ?AlbumWithFiles $album = null;
private IRequest $request;
private AlbumMapper $albumMapper;
private Throttler $throttler;
public function __construct(
IRequest $request,
AlbumMapper $albumMapper,
Throttler $throttler
) {
$this->request = $request;
$this->albumMapper = $albumMapper;
$this->throttler = $throttler;
// setup realm
$defaults = new \OCP\Defaults();
$this->realm = $defaults->getName();
}
/**
* Validates the token.
*
* @param string $username
* @return bool
* @throws \Sabre\DAV\Exception\NotAuthenticated
*/
protected function validateUserPass($username, $password) {
$this->throttler->sleepDelayOrThrowOnMax($this->request->getRemoteAddress(), self::BRUTEFORCE_ACTION);
$albums = $this->albumMapper->getSharedAlbumsForCollaboratorWithFiles($username, AlbumMapper::TYPE_LINK);
if (count($albums) !== 1) {
$this->throttler->registerAttempt(self::BRUTEFORCE_ACTION, $this->request->getRemoteAddress());
return false;
}
$this->album = $albums[0];
\OC_User::setIncognitoMode(true);
return true;
}
public function getShare(): AlbumWithFiles {
assert($this->album !== null);
return $this->album;
}
}

View File

@ -0,0 +1,87 @@
<?php
declare(strict_types=1);
/**
* @copyright Copyright (c) 2022 Robin Appelman <robin@icewind.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 OCA\Photos\Sabre;
use OCA\Photos\Album\AlbumMapper;
use OCA\Photos\Sabre\Album\PublicAlbumRoot;
use OCA\Photos\Service\UserConfigService;
use OCP\Files\IRootFolder;
use Sabre\DAVACL\AbstractPrincipalCollection;
use Sabre\DAVACL\PrincipalBackend;
use Sabre\DAV\Exception\NotFound;
class PublicRootCollection extends AbstractPrincipalCollection {
private AlbumMapper $albumMapper;
private IRootFolder $rootFolder;
private UserConfigService $userConfigService;
public function __construct(
AlbumMapper $albumMapper,
IRootFolder $rootFolder,
PrincipalBackend\BackendInterface $principalBackend,
UserConfigService $userConfigService
) {
parent::__construct($principalBackend, 'principals/token');
$this->albumMapper = $albumMapper;
$this->rootFolder = $rootFolder;
$this->userConfigService = $userConfigService;
}
public function getName(): string {
return 'photospublic';
}
/**
* Child are retrieved directly by getChild.
* This should never be called.
* @param array $principalInfo
*/
public function getChildForPrincipal(array $principalInfo): PublicAlbumRoot {
throw new \Sabre\DAV\Exception\Forbidden();
}
/**
* Returns a child object, by its token.
*
* @param string $token
*
* @throws NotFound
*
* @return DAV\INode
*/
public function getChild($token) {
if (is_null($token)) {
throw new \Sabre\DAV\Exception\Forbidden();
}
$albums = $this->albumMapper->getSharedAlbumsForCollaboratorWithFiles($token, AlbumMapper::TYPE_LINK);
if (count($albums) !== 1) {
throw new NotFound("Unable to find public album");
}
return new PublicAlbumRoot($this->albumMapper, $albums[0], $this->rootFolder, $albums[0]->getAlbum()->getUserId(), $this->userConfigService);
}
}

View File

@ -29,12 +29,14 @@ use OCP\Files\IRootFolder;
use OCP\IUserSession;
use Sabre\DAVACL\AbstractPrincipalCollection;
use Sabre\DAVACL\PrincipalBackend;
use OCP\IUserManager;
use OCP\IGroupManager;
class RootCollection extends AbstractPrincipalCollection {
private AlbumMapper $folderMapper;
private IUserSession $userSession;
private IRootFolder $rootFolder;
private IUserManager $userManager;
private IGroupManager $groupManager;
private UserConfigService $userConfigService;
@ -43,6 +45,7 @@ class RootCollection extends AbstractPrincipalCollection {
IUserSession $userSession,
IRootFolder $rootFolder,
PrincipalBackend\BackendInterface $principalBackend,
IUserManager $userManager,
IGroupManager $groupManager,
UserConfigService $userConfigService
) {
@ -51,6 +54,7 @@ class RootCollection extends AbstractPrincipalCollection {
$this->folderMapper = $folderMapper;
$this->userSession = $userSession;
$this->rootFolder = $rootFolder;
$this->userManager = $userManager;
$this->groupManager = $groupManager;
$this->userConfigService = $userConfigService;
}
@ -70,7 +74,7 @@ class RootCollection extends AbstractPrincipalCollection {
if (is_null($user) || $name !== $user->getUID()) {
throw new \Sabre\DAV\Exception\Forbidden();
}
return new PhotosHome($principalInfo, $this->folderMapper, $user, $this->rootFolder, $this->groupManager, $this->userConfigService);
return new PhotosHome($principalInfo, $this->folderMapper, $name, $this->rootFolder, $this->userManager, $this->groupManager, $this->userConfigService);
}
public function getName(): string {

View File

@ -1,13 +1,7 @@
<?xml version="1.0"?>
<psalm
errorLevel="4"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns="https://getpsalm.org/schema/config"
xsi:schemaLocation="https://getpsalm.org/schema/config vendor/vimeo/psalm/config.xsd"
errorBaseline="tests/psalm-baseline.xml"
>
<psalm errorLevel="4" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="https://getpsalm.org/schema/config" xsi:schemaLocation="https://getpsalm.org/schema/config vendor/vimeo/psalm/config.xsd" errorBaseline="tests/psalm-baseline.xml">
<stubs>
<file name="tests/stub.phpstub" preloadClasses="true"/>
<file name="tests/stub.phpstub" preloadClasses="true" />
</stubs>
<projectFiles>
<directory name="lib" />
@ -28,6 +22,9 @@
<referencedClass name="OCA\DAV\Connector\Sabre\Principal" />
<referencedClass name="OCA\DAV\Connector\Sabre\File" />
<referencedClass name="OCA\DAV\Connector\Sabre\FilesPlugin" />
<referencedClass name="OCA\DAV\Events\SabrePluginAuthInitEvent" />
<referencedClass name="OC\Security\Bruteforce\Throttler" />
<referencedClass name="OC_User" />
</errorLevel>
</UndefinedClass>
<UndefinedDocblockClass>

View File

@ -358,7 +358,7 @@ export default {
this.errorFetchingAlbum = error
}
logger.error('[PublicAlbumContent] Error fetching album', {error})
logger.error('[PublicAlbumContent] Error fetching album', { error })
showError(this.t('photos', 'Failed to fetch album.'))
} finally {
this.loadingAlbum = false
@ -385,7 +385,7 @@ export default {
},
})
} catch (error) {
logger.error('[PublicAlbumContent] Error updating album', {error})
logger.error('[PublicAlbumContent] Error updating album', { error })
showError(this.t('photos', 'Failed to update album.'))
} finally {
this.loadingAlbum = false
@ -393,7 +393,7 @@ export default {
},
async copyPublicLink() {
await navigator.clipboard.writeText(`${window.location.protocol}//${window.location.host}${generateUrl(`apps/photos/public/${getCurrentUser().uid}/${this.publicLink.id}`)}`)
await navigator.clipboard.writeText(`${window.location.protocol}//${window.location.host}${generateUrl(`apps/photos/public/${this.publicLink.id}`)}`)
this.publicLinkCopied = true
setTimeout(() => {
this.publicLinkCopied = false

View File

@ -201,7 +201,12 @@ export default {
},
getItemURL(size) {
const token = this.$route.params.token
if (token) {
return generateUrl(`/apps/photos/api/v1/publicPreview/${this.file.fileid}?x=${size}&y=${size}&token=${token}`)
} else {
return generateUrl(`/apps/photos/api/v1/preview/${this.file.fileid}?x=${size}&y=${size}`)
}
},

View File

@ -34,8 +34,8 @@ export default {
return {
croppedLayout: croppedLayoutLocalStorage !== null
? croppedLayoutLocalStorage === 'true'
: loadState('photos', 'croppedLayout') === 'true',
photosLocation: loadState('photos', 'photosLocation'),
: loadState('photos', 'croppedLayout', 'false') === 'true',
photosLocation: loadState('photos', 'photosLocation', ''),
}
},

View File

@ -120,11 +120,10 @@ const router = new Router({
}),
},
{
path: '/public/:userId/:token',
path: '/public/:token',
component: PublicAlbumContent,
name: 'publicAlbums',
props: route => ({
userId: route.params.userId,
token: route.params.token,
}),
},

View File

@ -23,7 +23,7 @@
import moment from '@nextcloud/moment'
import { translate } from '@nextcloud/l10n'
import client from '../services/DavClient.js'
import defaultClient from '../services/DavClient.js'
import logger from '../services/logger.js'
import DavRequest from '../services/DavRequest.js'
import { genFileInfo } from '../utils/fileUtils.js'
@ -65,9 +65,10 @@ function getDavRequest(extraProps = '') {
* @param {string} path - Albums' root path.
* @param {import('webdav').StatOptions} options - Options to forward to the webdav client.
* @param {string} extraProps - Extra properties to add to the DAV request.
* @param {import('webdav').WebDAVClient} client - The DAV client to use.
* @return {Promise<Album|null>}
*/
export async function fetchAlbum(path, options, extraProps = '') {
export async function fetchAlbum(path, options, extraProps = '', client = defaultClient) {
try {
const response = await client.stat(path, {
data: getDavRequest(extraProps),
@ -91,12 +92,14 @@ export async function fetchAlbum(path, options, extraProps = '') {
*
* @param {string} path - Albums' root path.
* @param {import('webdav').StatOptions} options - Options to forward to the webdav client.
* @param {string} extraProps - Extra properties to add to the DAV request.
* @param {import('webdav').WebDAVClient} client - The DAV client to use.
* @return {Promise<Album[]>}
*/
export async function fetchAlbums(path, options) {
export async function fetchAlbums(path, options, extraProps = '', client = defaultClient) {
try {
const response = await client.getDirectoryContents(path, {
data: getDavRequest(),
data: getDavRequest(extraProps),
details: true,
...options,
})
@ -158,9 +161,10 @@ function formatAlbum(album) {
*
* @param {string} path - Albums' root path.
* @param {import('webdav').StatOptions} options - Options to forward to the webdav client.
* @param {import('webdav').WebDAVClient} client - The DAV client to use.
* @return {Promise<Array>}
*/
export async function fetchAlbumContent(path, options) {
export async function fetchAlbumContent(path, options, client = defaultClient) {
try {
const response = await client.getDirectoryContents(path, {
data: DavRequest,

View File

@ -33,7 +33,7 @@
slot="header"
slot-scope="{selectedFileIds}"
:loading="loadingAlbum || loadingFiles"
:params="{ userId, token }"
:params="{ token }"
path="/"
:root-title="albumOriginalName"
:title="albumOriginalName"
@ -93,6 +93,8 @@
<script>
import { mapActions, mapGetters } from 'vuex'
import { createClient, getPatcher } from 'webdav'
import MapMarker from 'vue-material-design-icons/MapMarker'
import Plus from 'vue-material-design-icons/Plus'
import ImagePlus from 'vue-material-design-icons/ImagePlus'
@ -100,8 +102,10 @@ import Close from 'vue-material-design-icons/Close'
// import Download from 'vue-material-design-icons/Download'
// import DownloadMultiple from 'vue-material-design-icons/DownloadMultiple'
import { NcActions, NcActionButton, NcButton, NcModal, NcEmptyContent, /** NcActionSeparator, */ isMobile } from '@nextcloud/vue'
import { NcActions, NcActionButton, NcButton, NcEmptyContent, /** NcActionSeparator, */ isMobile } from '@nextcloud/vue'
import { showError } from '@nextcloud/dialogs'
import axios from '@nextcloud/axios'
import { generateRemoteUrl } from '@nextcloud/router'
import FetchFilesMixin from '../mixins/FetchFilesMixin.js'
import AbortControllerMixin from '../mixins/AbortControllerMixin.js'
@ -111,6 +115,16 @@ import HeaderNavigation from '../components/HeaderNavigation.vue'
import { fetchAlbum, fetchAlbumContent } from '../services/Albums.js'
import logger from '../services/logger.js'
const publicRootPath = 'dav'
// force our axios
const patcher = getPatcher()
patcher.patch('request', axios)
// init webdav client on default dav endpoint
const remote = generateRemoteUrl(publicRootPath)
const publicRemote = remote
export default {
name: 'PublicAlbumContent',
components: {
@ -137,10 +151,6 @@ export default {
],
props: {
userId: {
type: String,
required: true,
},
token: {
type: String,
required: true,
@ -155,6 +165,10 @@ export default {
loadingCount: 0,
loadingAddFilesToAlbum: false,
albumOriginalName: '',
publicClient: createClient(publicRemote, {
username: this.token,
password: null,
}),
}
},
@ -210,9 +224,10 @@ export default {
this.errorFetchingAlbum = null
const album = await fetchAlbum(
`/photos/${this.userId}/public/${this.token}`,
`/photospublic/${this.token}`,
this.abortController.signal,
'<nc:original-name />',
this.publicClient,
)
this.addPublicAlbums({ collections: [album] })
this.albumOriginalName = album.originalName
@ -244,8 +259,9 @@ export default {
this.semaphoreSymbol = semaphoreSymbol
const fetchedFiles = await fetchAlbumContent(
`/photos/${this.userId}/public/${this.token}`,
`/photospublic/${this.token}`,
this.abortController.signal,
this.publicClient,
)
const fileIds = fetchedFiles

26
templates/public.php Normal file
View File

@ -0,0 +1,26 @@
<?php
/**
* @copyright Copyright (c) 2019 John Molakvoæ <skjnldsv@protonmail.com>
*
* @author John Molakvoæ <skjnldsv@protonmail.com>
*
* @license AGPL-3.0-or-later
*
* 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/>.
*
*/
?>
<input type="hidden" id="isPublic" name="isPublic" value="1">
<div id="content"></div>

View File

@ -20,4 +20,10 @@
<code>SearchQuery</code>
</UndefinedClass>
</file>
<file src="lib/Controller/PublicAlbumController.php">
<UndefinedClass occurrences="10">
<code>LoadViewer</code>
<code>LoadViewer</code>
</UndefinedClass>
</file>
</files>