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

@ -10,9 +10,10 @@
<author mail="skjnldsv@protonmail.com">John Molakvoæ</author> <author mail="skjnldsv@protonmail.com">John Molakvoæ</author>
<namespace>Photos</namespace> <namespace>Photos</namespace>
<category>multimedia</category> <category>multimedia</category>
<types> <types>
<dav/> <dav />
</types> <authentication />
</types>
<website>https://github.com/nextcloud/photos</website> <website>https://github.com/nextcloud/photos</website>
<bugs>https://github.com/nextcloud/photos/issues</bugs> <bugs>https://github.com/nextcloud/photos/issues</bugs>
@ -29,12 +30,13 @@
</navigation> </navigation>
</navigations> </navigations>
<sabre> <sabre>
<collections> <collections>
<collection>OCA\Photos\Sabre\RootCollection</collection> <collection>OCA\Photos\Sabre\RootCollection</collection>
</collections> <collection>OCA\Photos\Sabre\PublicRootCollection</collection>
<plugins> </collections>
<plugin>OCA\Photos\Sabre\Album\PropFindPlugin</plugin> <plugins>
</plugins> <plugin>OCA\Photos\Sabre\Album\PropFindPlugin</plugin>
</sabre> </plugins>
</sabre>
</info> </info>

View File

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

View File

@ -362,7 +362,7 @@ class AlbumMapper {
} }
break; break;
case self::TYPE_LINK: 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; break;
default: default:
throw new \Exception('Invalid collaborator type: ' . $collaborator['type']); throw new \Exception('Invalid collaborator type: ' . $collaborator['type']);
@ -420,7 +420,13 @@ class AlbumMapper {
} }
if (!isset($albumsById[$albumId])) { 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 * @param int $fileId
* @return AlbumInfo[] * @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(); $query = $this->connection->getQueryBuilder();
$rows = $query $rows = $query
->select("a.album_id", "name", "user", "location", "created", "last_added_photo") ->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; namespace OCA\Photos\AppInfo;
use OCA\DAV\Events\SabrePluginAuthInitEvent;
use OCA\Photos\Listener\SabrePluginAuthInitListener;
use OCA\DAV\Connector\Sabre\Principal; use OCA\DAV\Connector\Sabre\Principal;
use OCA\Photos\Listener\CacheEntryRemovedListener; use OCA\Photos\Listener\CacheEntryRemovedListener;
use OCP\AppFramework\App; use OCP\AppFramework\App;
@ -64,6 +66,7 @@ class Application extends App implements IBootstrap {
/** Register $principalBackend for the DAV collection */ /** Register $principalBackend for the DAV collection */
$context->registerServiceAlias('principalBackend', Principal::class); $context->registerServiceAlias('principalBackend', Principal::class);
$context->registerEventListener(CacheEntryRemovedEvent::class, CacheEntryRemovedListener::class); $context->registerEventListener(CacheEntryRemovedEvent::class, CacheEntryRemovedListener::class);
$context->registerEventListener(SabrePluginAuthInitEvent::class, SabrePluginAuthInitListener::class);
} }
public function boot(IBootContext $context): void { public function boot(IBootContext $context): void {

View File

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

View File

@ -25,36 +25,27 @@
namespace OCA\Photos\Controller; namespace OCA\Photos\Controller;
use OCP\AppFramework\Controller; use OCP\AppFramework\Controller;
use OCA\Files\Event\LoadSidebar;
use OCA\Photos\AppInfo\Application; use OCA\Photos\AppInfo\Application;
use OCA\Photos\Service\UserConfigService;
use OCA\Viewer\Event\LoadViewer; use OCA\Viewer\Event\LoadViewer;
use OCP\App\IAppManager;
use OCP\AppFramework\Http\ContentSecurityPolicy; use OCP\AppFramework\Http\ContentSecurityPolicy;
use OCP\AppFramework\Http\TemplateResponse; use OCP\AppFramework\Http\Template\PublicTemplateResponse;
use OCP\EventDispatcher\IEventDispatcher; use OCP\EventDispatcher\IEventDispatcher;
use OCP\AppFramework\Services\IInitialState; use OCP\AppFramework\Services\IInitialState;
use OCP\IRequest; use OCP\IRequest;
use OCP\Util; use OCP\Util;
class PublicAlbumController extends Controller { class PublicAlbumController extends Controller {
private IAppManager $appManager;
private IEventDispatcher $eventDispatcher; private IEventDispatcher $eventDispatcher;
private UserConfigService $userConfig;
private IInitialState $initialState; private IInitialState $initialState;
public function __construct( public function __construct(
IRequest $request, IRequest $request,
IAppManager $appManager,
IEventDispatcher $eventDispatcher, IEventDispatcher $eventDispatcher,
UserConfigService $userConfig, IInitialState $initialState
IInitialState $initialState,
) { ) {
parent::__construct(Application::APP_ID, $request); parent::__construct(Application::APP_ID, $request);
$this->appManager = $appManager;
$this->eventDispatcher = $eventDispatcher; $this->eventDispatcher = $eventDispatcher;
$this->userConfig = $userConfig;
$this->initialState = $initialState; $this->initialState = $initialState;
} }
@ -62,7 +53,7 @@ class PublicAlbumController extends Controller {
* @PublicPage * @PublicPage
* @NoCSRFRequired * @NoCSRFRequired
*/ */
public function get(): TemplateResponse { public function get(): PublicTemplateResponse {
$this->eventDispatcher->dispatch(LoadViewer::class, new LoadViewer()); $this->eventDispatcher->dispatch(LoadViewer::class, new LoadViewer());
$this->initialState->provideInitialState('image-mimes', Application::IMAGE_MIMES); $this->initialState->provideInitialState('image-mimes', Application::IMAGE_MIMES);
@ -74,7 +65,7 @@ class PublicAlbumController extends Controller {
Util::addScript(Application::APP_ID, 'photos-public'); Util::addScript(Application::APP_ID, 'photos-public');
Util::addStyle(Application::APP_ID, 'icons'); Util::addStyle(Application::APP_ID, 'icons');
$response = new TemplateResponse(Application::APP_ID, 'main'); $response = new PublicTemplateResponse(Application::APP_ID, 'public');
$policy = new ContentSecurityPolicy(); $policy = new ContentSecurityPolicy();
$policy->addAllowedWorkerSrcDomain("'self'"); $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 { public function isFavorite(): bool {
$tagManager = \OCP\Server::get(\OCP\ITagManager::class); $tagManager = \OCP\Server::get(\OCP\ITagManager::class);
$tagger = $tagManager->load('files'); $tagger = $tagManager->load('files');
if ($tagger === null) {
return false;
}
$tags = $tagger->getTagsForObjects([$this->getFileId()]); $tags = $tagger->getTagsForObjects([$this->getFileId()]);
if ($tags === false || empty($tags)) { if ($tags === false || empty($tags)) {

View File

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

View File

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

View File

@ -28,13 +28,13 @@ use OCA\DAV\Connector\Sabre\FilesPlugin;
use OCA\Photos\Album\AlbumMapper; use OCA\Photos\Album\AlbumMapper;
use OCP\IConfig; use OCP\IConfig;
use OCP\IPreview; use OCP\IPreview;
use OCP\Files\NotFoundException;
use Sabre\DAV\INode; use Sabre\DAV\INode;
use Sabre\DAV\PropFind; use Sabre\DAV\PropFind;
use Sabre\DAV\PropPatch; use Sabre\DAV\PropPatch;
use Sabre\DAV\Server; use Sabre\DAV\Server;
use Sabre\DAV\ServerPlugin; use Sabre\DAV\ServerPlugin;
use Sabre\DAV\Tree; use Sabre\DAV\Tree;
use OCP\Files\NotFoundException;
class PropFindPlugin extends ServerPlugin { class PropFindPlugin extends ServerPlugin {
public const ORIGINAL_NAME_PROPERTYNAME = '{http://nextcloud.org/ns}original-name'; 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); $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 * @return void
*/ */

View File

@ -24,8 +24,6 @@ declare(strict_types=1);
namespace OCA\Photos\Sabre\Album; namespace OCA\Photos\Sabre\Album;
use Sabre\DAV\Exception\Forbidden; use Sabre\DAV\Exception\Forbidden;
use Sabre\DAV\Exception\Conflict;
use OCP\Files\Folder;
use Sabre\DAV\INode; use Sabre\DAV\INode;
class PublicAlbumRoot extends AlbumRoot { 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 * @return void
*/ */
public function delete() { 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'], fn ($collaborator) => $collaborator['type'].':'.$collaborator['id'],
$this->albumMapper->getCollaborators($this->album->getAlbum()->getId()), $this->albumMapper->getCollaborators($this->album->getAlbum()->getId()),
); );
$uid = $this->user->getUID(); if (!in_array(AlbumMapper::TYPE_USER.':'.$this->userId, $collaboratorIds)) {
if (!in_array(AlbumMapper::TYPE_USER.':'.$uid, $collaboratorIds)) {
return false; return false;
} }

View File

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

View File

@ -26,10 +26,9 @@ namespace OCA\Photos\Sabre;
use OCA\Photos\Album\AlbumMapper; use OCA\Photos\Album\AlbumMapper;
use OCA\Photos\Sabre\Album\AlbumsHome; use OCA\Photos\Sabre\Album\AlbumsHome;
use OCA\Photos\Sabre\Album\SharedAlbumsHome; use OCA\Photos\Sabre\Album\SharedAlbumsHome;
use OCA\Photos\Sabre\Album\PublicAlbumsHome;
use OCA\Photos\Service\UserConfigService; use OCA\Photos\Service\UserConfigService;
use OCP\Files\IRootFolder; use OCP\Files\IRootFolder;
use OCP\IUser; use OCP\IUserManager;
use OCP\IGroupManager; use OCP\IGroupManager;
use Sabre\DAV\Exception\Forbidden; use Sabre\DAV\Exception\Forbidden;
use Sabre\DAV\Exception\NotFound; use Sabre\DAV\Exception\NotFound;
@ -38,23 +37,26 @@ use Sabre\DAV\ICollection;
class PhotosHome implements ICollection { class PhotosHome implements ICollection {
private AlbumMapper $albumMapper; private AlbumMapper $albumMapper;
private array $principalInfo; private array $principalInfo;
private IUser $user; private string $userId;
private IRootFolder $rootFolder; private IRootFolder $rootFolder;
private IUserManager $userManager;
private IGroupManager $groupManager; private IGroupManager $groupManager;
private UserConfigService $userConfigService; private UserConfigService $userConfigService;
public function __construct( public function __construct(
array $principalInfo, array $principalInfo,
AlbumMapper $albumMapper, AlbumMapper $albumMapper,
IUser $user, string $userId,
IRootFolder $rootFolder, IRootFolder $rootFolder,
IUserManager $userManager,
IGroupManager $groupManager, IGroupManager $groupManager,
UserConfigService $userConfigService UserConfigService $userConfigService
) { ) {
$this->principalInfo = $principalInfo; $this->principalInfo = $principalInfo;
$this->albumMapper = $albumMapper; $this->albumMapper = $albumMapper;
$this->user = $user; $this->userId = $userId;
$this->rootFolder = $rootFolder; $this->rootFolder = $rootFolder;
$this->userManager = $userManager;
$this->groupManager = $groupManager; $this->groupManager = $groupManager;
$this->userConfigService = $userConfigService; $this->userConfigService = $userConfigService;
} }
@ -92,11 +94,9 @@ class PhotosHome implements ICollection {
public function getChild($name) { public function getChild($name) {
switch ($name) { switch ($name) {
case AlbumsHome::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: case SharedAlbumsHome::NAME:
return new SharedAlbumsHome($this->principalInfo, $this->albumMapper, $this->user, $this->rootFolder, $this->groupManager, $this->userConfigService); return new SharedAlbumsHome($this->principalInfo, $this->albumMapper, $this->userId, $this->rootFolder, $this->userManager, $this->groupManager, $this->userConfigService);
case PublicAlbumsHome::NAME:
return new PublicAlbumsHome($this->principalInfo, $this->albumMapper, $this->user, $this->rootFolder, $this->userConfigService);
} }
throw new NotFound(); throw new NotFound();
@ -107,14 +107,13 @@ class PhotosHome implements ICollection {
*/ */
public function getChildren(): array { public function getChildren(): array {
return [ return [
new AlbumsHome($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->user, $this->rootFolder, $this->groupManager, $this->userConfigService), new SharedAlbumsHome($this->principalInfo, $this->albumMapper, $this->userId, $this->rootFolder, $this->userManager, $this->groupManager, $this->userConfigService),
new PublicAlbumsHome($this->principalInfo, $this->albumMapper, $this->user, $this->rootFolder, $this->userConfigService),
]; ];
} }
public function childExists($name): bool { 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 { 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 OCP\IUserSession;
use Sabre\DAVACL\AbstractPrincipalCollection; use Sabre\DAVACL\AbstractPrincipalCollection;
use Sabre\DAVACL\PrincipalBackend; use Sabre\DAVACL\PrincipalBackend;
use OCP\IUserManager;
use OCP\IGroupManager; use OCP\IGroupManager;
class RootCollection extends AbstractPrincipalCollection { class RootCollection extends AbstractPrincipalCollection {
private AlbumMapper $folderMapper; private AlbumMapper $folderMapper;
private IUserSession $userSession; private IUserSession $userSession;
private IRootFolder $rootFolder; private IRootFolder $rootFolder;
private IUserManager $userManager;
private IGroupManager $groupManager; private IGroupManager $groupManager;
private UserConfigService $userConfigService; private UserConfigService $userConfigService;
@ -43,6 +45,7 @@ class RootCollection extends AbstractPrincipalCollection {
IUserSession $userSession, IUserSession $userSession,
IRootFolder $rootFolder, IRootFolder $rootFolder,
PrincipalBackend\BackendInterface $principalBackend, PrincipalBackend\BackendInterface $principalBackend,
IUserManager $userManager,
IGroupManager $groupManager, IGroupManager $groupManager,
UserConfigService $userConfigService UserConfigService $userConfigService
) { ) {
@ -51,6 +54,7 @@ class RootCollection extends AbstractPrincipalCollection {
$this->folderMapper = $folderMapper; $this->folderMapper = $folderMapper;
$this->userSession = $userSession; $this->userSession = $userSession;
$this->rootFolder = $rootFolder; $this->rootFolder = $rootFolder;
$this->userManager = $userManager;
$this->groupManager = $groupManager; $this->groupManager = $groupManager;
$this->userConfigService = $userConfigService; $this->userConfigService = $userConfigService;
} }
@ -70,7 +74,7 @@ class RootCollection extends AbstractPrincipalCollection {
if (is_null($user) || $name !== $user->getUID()) { if (is_null($user) || $name !== $user->getUID()) {
throw new \Sabre\DAV\Exception\Forbidden(); 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 { public function getName(): string {

View File

@ -1,13 +1,7 @@
<?xml version="1.0"?> <?xml version="1.0"?>
<psalm <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">
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> <stubs>
<file name="tests/stub.phpstub" preloadClasses="true"/> <file name="tests/stub.phpstub" preloadClasses="true" />
</stubs> </stubs>
<projectFiles> <projectFiles>
<directory name="lib" /> <directory name="lib" />
@ -28,6 +22,9 @@
<referencedClass name="OCA\DAV\Connector\Sabre\Principal" /> <referencedClass name="OCA\DAV\Connector\Sabre\Principal" />
<referencedClass name="OCA\DAV\Connector\Sabre\File" /> <referencedClass name="OCA\DAV\Connector\Sabre\File" />
<referencedClass name="OCA\DAV\Connector\Sabre\FilesPlugin" /> <referencedClass name="OCA\DAV\Connector\Sabre\FilesPlugin" />
<referencedClass name="OCA\DAV\Events\SabrePluginAuthInitEvent" />
<referencedClass name="OC\Security\Bruteforce\Throttler" />
<referencedClass name="OC_User" />
</errorLevel> </errorLevel>
</UndefinedClass> </UndefinedClass>
<UndefinedDocblockClass> <UndefinedDocblockClass>

View File

@ -358,7 +358,7 @@ export default {
this.errorFetchingAlbum = error 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.')) showError(this.t('photos', 'Failed to fetch album.'))
} finally { } finally {
this.loadingAlbum = false this.loadingAlbum = false
@ -385,7 +385,7 @@ export default {
}, },
}) })
} catch (error) { } catch (error) {
logger.error('[PublicAlbumContent] Error updating album', {error}) logger.error('[PublicAlbumContent] Error updating album', { error })
showError(this.t('photos', 'Failed to update album.')) showError(this.t('photos', 'Failed to update album.'))
} finally { } finally {
this.loadingAlbum = false this.loadingAlbum = false
@ -393,7 +393,7 @@ export default {
}, },
async copyPublicLink() { 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 this.publicLinkCopied = true
setTimeout(() => { setTimeout(() => {
this.publicLinkCopied = false this.publicLinkCopied = false

View File

@ -201,7 +201,12 @@ export default {
}, },
getItemURL(size) { getItemURL(size) {
return generateUrl(`/apps/photos/api/v1/preview/${this.file.fileid}?x=${size}&y=${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 { return {
croppedLayout: croppedLayoutLocalStorage !== null croppedLayout: croppedLayoutLocalStorage !== null
? croppedLayoutLocalStorage === 'true' ? croppedLayoutLocalStorage === 'true'
: loadState('photos', 'croppedLayout') === 'true', : loadState('photos', 'croppedLayout', 'false') === 'true',
photosLocation: loadState('photos', 'photosLocation'), photosLocation: loadState('photos', 'photosLocation', ''),
} }
}, },

View File

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

View File

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

View File

@ -33,7 +33,7 @@
slot="header" slot="header"
slot-scope="{selectedFileIds}" slot-scope="{selectedFileIds}"
:loading="loadingAlbum || loadingFiles" :loading="loadingAlbum || loadingFiles"
:params="{ userId, token }" :params="{ token }"
path="/" path="/"
:root-title="albumOriginalName" :root-title="albumOriginalName"
:title="albumOriginalName" :title="albumOriginalName"
@ -93,6 +93,8 @@
<script> <script>
import { mapActions, mapGetters } from 'vuex' import { mapActions, mapGetters } from 'vuex'
import { createClient, getPatcher } from 'webdav'
import MapMarker from 'vue-material-design-icons/MapMarker' import MapMarker from 'vue-material-design-icons/MapMarker'
import Plus from 'vue-material-design-icons/Plus' import Plus from 'vue-material-design-icons/Plus'
import ImagePlus from 'vue-material-design-icons/ImagePlus' 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 Download from 'vue-material-design-icons/Download'
// import DownloadMultiple from 'vue-material-design-icons/DownloadMultiple' // 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 { showError } from '@nextcloud/dialogs'
import axios from '@nextcloud/axios'
import { generateRemoteUrl } from '@nextcloud/router'
import FetchFilesMixin from '../mixins/FetchFilesMixin.js' import FetchFilesMixin from '../mixins/FetchFilesMixin.js'
import AbortControllerMixin from '../mixins/AbortControllerMixin.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 { fetchAlbum, fetchAlbumContent } from '../services/Albums.js'
import logger from '../services/logger.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 { export default {
name: 'PublicAlbumContent', name: 'PublicAlbumContent',
components: { components: {
@ -137,10 +151,6 @@ export default {
], ],
props: { props: {
userId: {
type: String,
required: true,
},
token: { token: {
type: String, type: String,
required: true, required: true,
@ -155,6 +165,10 @@ export default {
loadingCount: 0, loadingCount: 0,
loadingAddFilesToAlbum: false, loadingAddFilesToAlbum: false,
albumOriginalName: '', albumOriginalName: '',
publicClient: createClient(publicRemote, {
username: this.token,
password: null,
}),
} }
}, },
@ -210,9 +224,10 @@ export default {
this.errorFetchingAlbum = null this.errorFetchingAlbum = null
const album = await fetchAlbum( const album = await fetchAlbum(
`/photos/${this.userId}/public/${this.token}`, `/photospublic/${this.token}`,
this.abortController.signal, this.abortController.signal,
'<nc:original-name />', '<nc:original-name />',
this.publicClient,
) )
this.addPublicAlbums({ collections: [album] }) this.addPublicAlbums({ collections: [album] })
this.albumOriginalName = album.originalName this.albumOriginalName = album.originalName
@ -244,8 +259,9 @@ export default {
this.semaphoreSymbol = semaphoreSymbol this.semaphoreSymbol = semaphoreSymbol
const fetchedFiles = await fetchAlbumContent( const fetchedFiles = await fetchAlbumContent(
`/photos/${this.userId}/public/${this.token}`, `/photospublic/${this.token}`,
this.abortController.signal, this.abortController.signal,
this.publicClient,
) )
const fileIds = fetchedFiles 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> <code>SearchQuery</code>
</UndefinedClass> </UndefinedClass>
</file> </file>
</files> <file src="lib/Controller/PublicAlbumController.php">
<UndefinedClass occurrences="10">
<code>LoadViewer</code>
<code>LoadViewer</code>
</UndefinedClass>
</file>
</files>