parent
d8b4caf4aa
commit
24a3b8c638
|
@ -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
|
||||
|
|
|
@ -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));
|
||||
}
|
||||
|
||||
|
@ -30,15 +32,18 @@ return [
|
|||
// Public folder share
|
||||
['name' => 'Public#showShare', 'url' => '/s/{token}', 'verb' => 'GET'],
|
||||
[
|
||||
'name' => 'Public#showAuthenticate',
|
||||
'url' => '/s/{token}/authenticate/{redirect}',
|
||||
'verb' => 'GET',
|
||||
],
|
||||
[
|
||||
'name' => 'Public#authenticate',
|
||||
'url' => '/s/{token}/authenticate/{redirect}',
|
||||
'verb' => 'POST',
|
||||
],
|
||||
'name' => 'Public#showAuthenticate',
|
||||
'url' => '/s/{token}/authenticate/{redirect}',
|
||||
'verb' => 'GET',
|
||||
],
|
||||
[
|
||||
'name' => 'Public#authenticate',
|
||||
'url' => '/s/{token}/authenticate/{redirect}',
|
||||
'verb' => 'POST',
|
||||
],
|
||||
|
||||
// Public album share
|
||||
['name' => 'PublicAlbum#showShare', 'url' => '/a/{token}', 'verb' => 'GET'],
|
||||
|
||||
// API Routes
|
||||
['name' => 'Days#days', 'url' => '/api/days', 'verb' => 'GET'],
|
||||
|
|
|
@ -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,11 +80,17 @@ 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')) {
|
||||
return $root;
|
||||
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
|
||||
|
@ -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');
|
||||
}
|
||||
|
@ -143,7 +147,7 @@ class ApiBase extends Controller
|
|||
|
||||
// Check both user folder and album
|
||||
return $this->getUserFolderFile($fileId) ??
|
||||
$this->getAlbumFile($fileId);
|
||||
$this->getAlbumFile($fileId);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -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');
|
||||
}
|
||||
}
|
||||
|
|
|
@ -39,7 +39,11 @@ class DaysController extends ApiBase
|
|||
public function days(): JSONResponse
|
||||
{
|
||||
// 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
|
||||
$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) {
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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,25 +177,28 @@ trait TimelineQueryAlbums
|
|||
*/
|
||||
public function getAlbumIfAllowed(string $uid, string $albumId)
|
||||
{
|
||||
$album = null;
|
||||
|
||||
// Split name and uid
|
||||
$parts = explode('/', $albumId);
|
||||
if (2 !== \count($parts)) {
|
||||
return null;
|
||||
}
|
||||
$albumUid = $parts[0];
|
||||
$albumName = $parts[1];
|
||||
if (2 === \count($parts)) {
|
||||
$albumUid = $parts[0];
|
||||
$albumName = $parts[1];
|
||||
|
||||
// Check if owner
|
||||
$query = $this->connection->getQueryBuilder();
|
||||
$query->select('*')->from('photos_albums')->where(
|
||||
$query->expr()->andX(
|
||||
$query->expr()->eq('name', $query->createNamedParameter($albumName)),
|
||||
$query->expr()->eq('user', $query->createNamedParameter($albumUid)),
|
||||
)
|
||||
);
|
||||
$album = $query->executeQuery()->fetch();
|
||||
// Check if owner
|
||||
$query = $this->connection->getQueryBuilder();
|
||||
$query->select('*')->from('photos_albums')->where(
|
||||
$query->expr()->andX(
|
||||
$query->expr()->eq('name', $query->createNamedParameter($albumName)),
|
||||
$query->expr()->eq('user', $query->createNamedParameter($albumUid)),
|
||||
)
|
||||
);
|
||||
$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.
|
||||
*/
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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 */
|
||||
|
|
|
@ -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 {
|
||||
|
@ -1408,4 +1410,4 @@ export default defineComponent({
|
|||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</style>
|
||||
|
|
|
@ -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;
|
||||
|
@ -553,4 +553,4 @@ export default defineComponent({
|
|||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</style>
|
||||
|
|
|
@ -9,6 +9,7 @@
|
|||
:album-name="album.basename"
|
||||
:collaborators="album.collaborators"
|
||||
:public-link="album.publicLink"
|
||||
:allow-public-link="true"
|
||||
v-slot="{ collaborators }"
|
||||
>
|
||||
<NcButton
|
||||
|
@ -85,4 +86,4 @@ export default defineComponent({
|
|||
},
|
||||
},
|
||||
});
|
||||
</script>
|
||||
</script>
|
||||
|
|
|
@ -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 */
|
||||
|
|
|
@ -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"),
|
||||
}),
|
||||
},
|
||||
],
|
||||
});
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue