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)
- **Feature**: Allow sharing albums using public links
- **Feature**: Allow sharing albums with groups
- Fix folder share title and remove footer
- Other minor fixes

View File

@ -1,13 +1,15 @@
<?php
function getWildcard($param) {
function getWildcard($param)
{
return [
'requirements' => [ $param => '.*' ],
'defaults' => [ $param => '' ]
'requirements' => [$param => '.*'],
'defaults' => [$param => '']
];
}
function w($base, $param) {
function w($base, $param)
{
return array_merge($base, getWildcard($param));
}
@ -40,6 +42,9 @@ return [
'verb' => 'POST',
],
// Public album share
['name' => 'PublicAlbum#showShare', 'url' => '/a/{token}', 'verb' => 'GET'],
// API Routes
['name' => 'Days#days', 'url' => '/api/days', 'verb' => 'GET'],
['name' => 'Days#day', 'url' => '/api/days/{id}', 'verb' => 'GET'],

View File

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

View File

@ -39,7 +39,11 @@ class DaysController extends ApiBase
public function days(): JSONResponse
{
// Get the folder to show
try {
$uid = $this->getUID();
} catch (\Exception $e) {
return new JSONResponse(['message' => $e->getMessage()], Http::STATUS_PRECONDITION_FAILED);
}
// Get the folder to show
$root = null;
@ -183,6 +187,13 @@ class DaysController extends ApiBase
$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
if (null === $this->userSession->getUser()) {
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 = $this->request->getParam('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;
}
/**
* 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.
*
@ -159,11 +177,11 @@ trait TimelineQueryAlbums
*/
public function getAlbumIfAllowed(string $uid, string $albumId)
{
$album = null;
// Split name and uid
$parts = explode('/', $albumId);
if (2 !== \count($parts)) {
return null;
}
if (2 === \count($parts)) {
$albumUid = $parts[0];
$albumName = $parts[1];
@ -176,8 +194,11 @@ trait TimelineQueryAlbums
)
);
$album = $query->executeQuery()->fetch();
}
// Album not found: it could be a link token at best
if (!$album) {
return null;
return $this->getAlbumByLink($albumId);
}
// 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.
*/

View File

@ -140,7 +140,7 @@ export default defineComponent({
},
showNavigation(): boolean {
return this.$route.name !== "folder-share";
return !this.$route.name?.endsWith("-share");
},
},
@ -286,12 +286,12 @@ export default defineComponent({
},
doRouteChecks() {
if (this.$route.name === "folder-share") {
this.putFolderShareToken(<string>this.$route.params.token);
if (this.$route.name.endsWith("-share")) {
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
// Create this element or update it otherwise files not gonna open
// 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 */
routeIsPublic() {
return this.$route.name === "folder-share";
return this.$route.name?.endsWith("-share");
},
/** Trigger to update props from selection set */

View File

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

View File

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

View File

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

View File

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

View File

@ -120,5 +120,14 @@ export default new Router({
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();
if (route.name === "folder-share") {
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;
}