Fix album public link (fix #344, fix #274)

pull/363/head
Varun Patil 2023-01-17 19:02:00 -08:00
parent d8b4caf4aa
commit 24a3b8c638
14 changed files with 236 additions and 59 deletions

View File

@ -4,6 +4,7 @@ This file is manually updated. Please file an issue if something is missing.
## v4.10.0, v3.10.0 (unreleased) ## v4.10.0, v3.10.0 (unreleased)
- **Feature**: Allow sharing albums using public links
- **Feature**: Allow sharing albums with groups - **Feature**: Allow sharing albums with groups
- Fix folder share title and remove footer - Fix folder share title and remove footer
- Other minor fixes - Other minor fixes

View File

@ -1,13 +1,15 @@
<?php <?php
function getWildcard($param) { function getWildcard($param)
{
return [ return [
'requirements' => [ $param => '.*' ], 'requirements' => [$param => '.*'],
'defaults' => [ $param => '' ] 'defaults' => [$param => '']
]; ];
} }
function w($base, $param) { function w($base, $param)
{
return array_merge($base, getWildcard($param)); return array_merge($base, getWildcard($param));
} }
@ -30,15 +32,18 @@ return [
// Public folder share // Public folder share
['name' => 'Public#showShare', 'url' => '/s/{token}', 'verb' => 'GET'], ['name' => 'Public#showShare', 'url' => '/s/{token}', 'verb' => 'GET'],
[ [
'name' => 'Public#showAuthenticate', 'name' => 'Public#showAuthenticate',
'url' => '/s/{token}/authenticate/{redirect}', 'url' => '/s/{token}/authenticate/{redirect}',
'verb' => 'GET', 'verb' => 'GET',
], ],
[ [
'name' => 'Public#authenticate', 'name' => 'Public#authenticate',
'url' => '/s/{token}/authenticate/{redirect}', 'url' => '/s/{token}/authenticate/{redirect}',
'verb' => 'POST', 'verb' => 'POST',
], ],
// Public album share
['name' => 'PublicAlbum#showShare', 'url' => '/a/{token}', 'verb' => 'GET'],
// API Routes // API Routes
['name' => 'Days#days', 'url' => '/api/days', 'verb' => 'GET'], ['name' => 'Days#days', 'url' => '/api/days', 'verb' => 'GET'],

View File

@ -29,8 +29,6 @@ use OCA\Memories\Db\TimelineRoot;
use OCA\Memories\Exif; use OCA\Memories\Exif;
use OCP\App\IAppManager; use OCP\App\IAppManager;
use OCP\AppFramework\Controller; use OCP\AppFramework\Controller;
use OCP\AppFramework\Http;
use OCP\AppFramework\Http\JSONResponse;
use OCP\Files\File; use OCP\Files\File;
use OCP\Files\Folder; use OCP\Files\Folder;
use OCP\Files\IRootFolder; use OCP\Files\IRootFolder;
@ -46,6 +44,7 @@ class ApiBase extends Controller
protected IRootFolder $rootFolder; protected IRootFolder $rootFolder;
protected IAppManager $appManager; protected IAppManager $appManager;
protected TimelineQuery $timelineQuery; protected TimelineQuery $timelineQuery;
protected IDBConnection $connection;
public function __construct( public function __construct(
IRequest $request, IRequest $request,
@ -65,14 +64,14 @@ class ApiBase extends Controller
$this->timelineQuery = new TimelineQuery($connection); $this->timelineQuery = new TimelineQuery($connection);
} }
/** Get logged in user's UID or throw HTTP error */ /** Get logged in user's UID or throw exception */
protected function getUID(): string protected function getUID(): string
{ {
$user = $this->userSession->getUser(); $user = $this->userSession->getUser();
if ($this->getShareToken()) { if ($this->getShareToken()) {
$user = null; $user = null;
} elseif (null === $user) { } elseif (null === $user) {
return new JSONResponse([], Http::STATUS_PRECONDITION_FAILED); throw new \Exception('User not logged in');
} }
return $user ? $user->getUID() : ''; return $user ? $user->getUID() : '';
@ -81,11 +80,17 @@ class ApiBase extends Controller
/** Get the TimelineRoot object relevant to the request */ /** Get the TimelineRoot object relevant to the request */
protected function getRequestRoot() protected function getRequestRoot()
{ {
$user = $this->userSession->getUser();
$root = new TimelineRoot(); $root = new TimelineRoot();
// Albums have no folder // Albums have no folder
if ($this->request->getParam('album')) { if ($this->albumsIsEnabled() && $this->request->getParam('album')) {
return $root; if (null !== $user) {
return $root;
}
if (($token = $this->getShareToken()) && $this->timelineQuery->getAlbumByLink($token)) {
return $root;
}
} }
// Public shared folder // Public shared folder
@ -96,7 +101,6 @@ class ApiBase extends Controller
} }
// Anything else needs a user // Anything else needs a user
$user = $this->userSession->getUser();
if (null === $user) { if (null === $user) {
throw new \Exception('User not logged in'); throw new \Exception('User not logged in');
} }
@ -143,7 +147,7 @@ class ApiBase extends Controller
// Check both user folder and album // Check both user folder and album
return $this->getUserFolderFile($fileId) ?? return $this->getUserFolderFile($fileId) ??
$this->getAlbumFile($fileId); $this->getAlbumFile($fileId);
} }
/** /**
@ -189,6 +193,24 @@ class ApiBase extends Controller
protected function getShareFile(int $id): ?File protected function getShareFile(int $id): ?File
{ {
try { try {
// Album share
if ($this->request->getParam('album')) {
$album = $this->timelineQuery->getAlbumByLink($this->getShareToken());
if (null === $album) {
return null;
}
$owner = $this->timelineQuery->albumHasFile($album['album_id'], $id);
if (!$owner) {
return null;
}
$folder = $this->rootFolder->getUserFolder($owner);
return $this->getOneFileFromFolder($folder, $id);
}
// Folder share
if ($share = $this->getShareNode()) { if ($share = $this->getShareNode()) {
return $this->getOneFileFromFolder($share, $id); return $this->getOneFileFromFolder($share, $id);
} }
@ -220,7 +242,7 @@ class ApiBase extends Controller
protected function getShareToken() protected function getShareToken()
{ {
return $this->request->getParam('folder_share'); return $this->request->getParam('token');
} }
protected function getShareObject() protected function getShareObject()
@ -242,8 +264,10 @@ class ApiBase extends Controller
$session = \OC::$server->get(\OCP\ISession::class); $session = \OC::$server->get(\OCP\ISession::class);
// https://github.com/nextcloud/server/blob/0447b53bda9fe95ea0cbed765aa332584605d652/lib/public/AppFramework/PublicShareController.php#L119 // https://github.com/nextcloud/server/blob/0447b53bda9fe95ea0cbed765aa332584605d652/lib/public/AppFramework/PublicShareController.php#L119
if ($session->get('public_link_authenticated_token') !== $token if (
|| $session->get('public_link_authenticated_password_hash') !== $password) { $session->get('public_link_authenticated_token') !== $token
|| $session->get('public_link_authenticated_password_hash') !== $password
) {
throw new \Exception('Share is password protected and user is not authenticated'); throw new \Exception('Share is password protected and user is not authenticated');
} }
} }

View File

@ -39,7 +39,11 @@ class DaysController extends ApiBase
public function days(): JSONResponse public function days(): JSONResponse
{ {
// Get the folder to show // Get the folder to show
$uid = $this->getUID(); try {
$uid = $this->getUID();
} catch (\Exception $e) {
return new JSONResponse(['message' => $e->getMessage()], Http::STATUS_PRECONDITION_FAILED);
}
// Get the folder to show // Get the folder to show
$root = null; $root = null;
@ -183,6 +187,13 @@ class DaysController extends ApiBase
$transforms[] = [$this->timelineQuery, 'transformExtraFields', $fields]; $transforms[] = [$this->timelineQuery, 'transformExtraFields', $fields];
} }
// Filter for one album
if ($this->albumsIsEnabled()) {
if ($albumId = $this->request->getParam('album')) {
$transforms[] = [$this->timelineQuery, 'transformAlbumFilter', $albumId];
}
}
// Other transforms not allowed for public shares // Other transforms not allowed for public shares
if (null === $this->userSession->getUser()) { if (null === $this->userSession->getUser()) {
return $transforms; return $transforms;
@ -226,13 +237,6 @@ class DaysController extends ApiBase
} }
} }
// Filter for one album
if ($this->albumsIsEnabled()) {
if ($albumId = $this->request->getParam('album')) {
$transforms[] = [$this->timelineQuery, 'transformAlbumFilter', $albumId];
}
}
// Limit number of responses for day query // Limit number of responses for day query
$limit = $this->request->getParam('limit'); $limit = $this->request->getParam('limit');
if ($limit) { if ($limit) {

View File

@ -0,0 +1,89 @@
<?php
namespace OCA\Memories\Controller;
use OCA\Files\Event\LoadSidebar;
use OCA\Memories\Db\TimelineQuery;
use OCP\App\IAppManager;
use OCP\AppFramework\Controller;
use OCP\AppFramework\Http\ContentSecurityPolicy;
use OCP\AppFramework\Http\Template\PublicTemplateResponse;
use OCP\AppFramework\Http\TemplateResponse;
use OCP\AppFramework\Services\IInitialState;
use OCP\EventDispatcher\IEventDispatcher;
use OCP\IConfig;
use OCP\IDBConnection;
use OCP\Util;
class PublicAlbumController extends Controller
{
protected $appName;
protected IEventDispatcher $eventDispatcher;
protected IInitialState $initialState;
protected IAppManager $appManager;
protected IConfig $config;
protected IDBConnection $connection;
public function __construct(
string $appName,
IEventDispatcher $eventDispatcher,
IInitialState $initialState,
IAppManager $appManager,
IConfig $config,
IDBConnection $connection
) {
$this->appName = $appName;
$this->eventDispatcher = $eventDispatcher;
$this->initialState = $initialState;
$this->appManager = $appManager;
$this->config = $config;
$this->connection = $connection;
}
/**
* @PublicPage
*
* @NoCSRFRequired
*/
public function showShare(string $token): TemplateResponse
{
\OC_User::setIncognitoMode(true);
// Validate token exists
$timelineQuery = new TimelineQuery($this->connection);
$album = $timelineQuery->getAlbumByLink($token);
if (!$album) {
return new TemplateResponse('core', '404', [], 'guest');
}
// Scripts
Util::addScript($this->appName, 'memories-main');
$this->eventDispatcher->dispatchTyped(new LoadSidebar());
$this->initialState->provideInitialState('version', $this->appManager->getAppInfo('memories')['version']);
$this->initialState->provideInitialState('notranscode', $this->config->getSystemValue('memories.no_transcode', 'UNSET'));
$policy = new ContentSecurityPolicy();
$policy->addAllowedWorkerSrcDomain("'self'");
$policy->addAllowedScriptDomain("'self'");
// Video player
$policy->addAllowedWorkerSrcDomain('blob:');
$policy->addAllowedScriptDomain('blob:');
$policy->addAllowedMediaDomain('blob:');
// Image editor
$policy->addAllowedConnectDomain('data:');
// Allow nominatim for metadata
$policy->addAllowedConnectDomain('nominatim.openstreetmap.org');
$policy->addAllowedFrameDomain('www.openstreetmap.org');
$response = new PublicTemplateResponse($this->appName, 'main');
$response->setHeaderTitle($album['name']);
$response->setFooterVisible(false); // wth is that anyway?
$response->setContentSecurityPolicy($policy);
return $response;
}
}

View File

@ -117,6 +117,24 @@ trait TimelineQueryAlbums
return $dayIds; return $dayIds;
} }
/**
* Check if an album has a file.
*
* @return bool|string owner of file
*/
public function albumHasFile(int $albumId, int $fileId)
{
$query = $this->connection->getQueryBuilder();
$query->select('owner')->from('photos_albums_files')->where(
$query->expr()->andX(
$query->expr()->eq('file_id', $query->createNamedParameter($fileId, IQueryBuilder::PARAM_INT)),
$query->expr()->eq('album_id', $query->createNamedParameter($albumId, IQueryBuilder::PARAM_INT)),
)
);
return $query->executeQuery()->fetchOne();
}
/** /**
* Check if a file belongs to a user through an album. * Check if a file belongs to a user through an album.
* *
@ -159,25 +177,28 @@ trait TimelineQueryAlbums
*/ */
public function getAlbumIfAllowed(string $uid, string $albumId) public function getAlbumIfAllowed(string $uid, string $albumId)
{ {
$album = null;
// Split name and uid // Split name and uid
$parts = explode('/', $albumId); $parts = explode('/', $albumId);
if (2 !== \count($parts)) { if (2 === \count($parts)) {
return null; $albumUid = $parts[0];
} $albumName = $parts[1];
$albumUid = $parts[0];
$albumName = $parts[1];
// Check if owner // Check if owner
$query = $this->connection->getQueryBuilder(); $query = $this->connection->getQueryBuilder();
$query->select('*')->from('photos_albums')->where( $query->select('*')->from('photos_albums')->where(
$query->expr()->andX( $query->expr()->andX(
$query->expr()->eq('name', $query->createNamedParameter($albumName)), $query->expr()->eq('name', $query->createNamedParameter($albumName)),
$query->expr()->eq('user', $query->createNamedParameter($albumUid)), $query->expr()->eq('user', $query->createNamedParameter($albumUid)),
) )
); );
$album = $query->executeQuery()->fetch(); $album = $query->executeQuery()->fetch();
}
// Album not found: it could be a link token at best
if (!$album) { if (!$album) {
return null; return $this->getAlbumByLink($albumId);
} }
// Check if user is owner // Check if user is owner
@ -200,6 +221,24 @@ trait TimelineQueryAlbums
} }
} }
/**
* Get album object by token.
* Returns false if album link does not exist.
*/
public function getAlbumByLink(string $token)
{
$query = $this->connection->getQueryBuilder();
$query->select('*')->from('photos_albums', 'pa')
->innerJoin('pa', $this->collaboratorsTable(), 'pc', $query->expr()->andX(
$query->expr()->eq('pc.album_id', 'pa.album_id'),
$query->expr()->eq('collaborator_id', $query->createNamedParameter($token)),
$query->expr()->eq('collaborator_type', $query->createNamedParameter(3)), // = TYPE_LINK
))
;
return $query->executeQuery()->fetch() ?: null;
}
/** /**
* Get full list of fileIds in album. * Get full list of fileIds in album.
*/ */

View File

@ -140,7 +140,7 @@ export default defineComponent({
}, },
showNavigation(): boolean { showNavigation(): boolean {
return this.$route.name !== "folder-share"; return !this.$route.name?.endsWith("-share");
}, },
}, },
@ -286,12 +286,12 @@ export default defineComponent({
}, },
doRouteChecks() { doRouteChecks() {
if (this.$route.name === "folder-share") { if (this.$route.name.endsWith("-share")) {
this.putFolderShareToken(<string>this.$route.params.token); this.putShareToken(<string>this.$route.params.token);
} }
}, },
putFolderShareToken(token: string) { putShareToken(token: string) {
// Viewer looks for an input with ID sharingToken with the value as the token // Viewer looks for an input with ID sharingToken with the value as the token
// Create this element or update it otherwise files not gonna open // Create this element or update it otherwise files not gonna open
// https://github.com/nextcloud/viewer/blob/a8c46050fb687dcbb48a022a15a5d1275bf54a8e/src/utils/davUtils.js#L61 // https://github.com/nextcloud/viewer/blob/a8c46050fb687dcbb48a022a15a5d1275bf54a8e/src/utils/davUtils.js#L61

View File

@ -264,7 +264,7 @@ export default defineComponent({
/** Public route that can't modify anything */ /** Public route that can't modify anything */
routeIsPublic() { routeIsPublic() {
return this.$route.name === "folder-share"; return this.$route.name?.endsWith("-share");
}, },
/** Trigger to update props from selection set */ /** Trigger to update props from selection set */

View File

@ -266,7 +266,9 @@ export default defineComponent({
return this.$route.name === "archive"; return this.$route.name === "archive";
}, },
isMonthView(): boolean { isMonthView(): boolean {
return this.$route.name === "albums"; return (
this.$route.name === "albums" || this.$route.name === "album-share"
);
}, },
/** Get view name for dynamic top matter */ /** Get view name for dynamic top matter */
viewName(): string { viewName(): string {

View File

@ -421,7 +421,7 @@ export default defineComponent({
async copyPublicLink() { async copyPublicLink() {
await navigator.clipboard.writeText( await navigator.clipboard.writeText(
`${window.location.protocol}//${window.location.host}${generateUrl( `${window.location.protocol}//${window.location.host}${generateUrl(
`apps/photos/public/${this.publicLink.id}` `apps/memories/a/${this.publicLink.id}`
)}` )}`
); );
this.publicLinkCopied = true; this.publicLinkCopied = true;

View File

@ -9,6 +9,7 @@
:album-name="album.basename" :album-name="album.basename"
:collaborators="album.collaborators" :collaborators="album.collaborators"
:public-link="album.publicLink" :public-link="album.publicLink"
:allow-public-link="true"
v-slot="{ collaborators }" v-slot="{ collaborators }"
> >
<NcButton <NcButton

View File

@ -283,7 +283,7 @@ export default defineComponent({
/** Route is public */ /** Route is public */
routeIsPublic(): boolean { routeIsPublic(): boolean {
return this.$route.name === "folder-share"; return this.$route.name?.endsWith("-share");
}, },
/** Route is album */ /** Route is album */

View File

@ -120,5 +120,14 @@ export default new Router({
rootTitle: t("memories", "Shared Folder"), rootTitle: t("memories", "Shared Folder"),
}), }),
}, },
{
path: "/a/:token",
component: Timeline,
name: "album-share",
props: (route) => ({
rootTitle: t("memories", "Shared Album"),
}),
},
], ],
}); });

View File

@ -9,7 +9,10 @@ function tok(url: string) {
const route = vueroute(); const route = vueroute();
if (route.name === "folder-share") { if (route.name === "folder-share") {
const token = <string>route.params.token; const token = <string>route.params.token;
url = API.Q(url, `folder_share=${token}`); url = API.Q(url, `token=${token}`);
} else if (route.name === "album-share") {
const token = <string>route.params.token;
url = API.Q(url, `token=${token}&album=${token}`);
} }
return url; return url;
} }