From b7e2b2345af65b2436f3644bc155baeaa6cd17b9 Mon Sep 17 00:00:00 2001 From: Matthias Nagel Date: Tue, 15 Nov 2022 14:49:49 +0100 Subject: [PATCH] Fix list of shares (#1590) --- app/Actions/Sharing/ListShare.php | 57 ++++++++----- .../Administration/SharingController.php | 2 +- .../Requests/Sharing/ListSharingRequest.php | 65 +++++++++++--- public/Lychee-front | 2 +- public/dist/main.js | 84 +++++++++++++++---- 5 files changed, 160 insertions(+), 50 deletions(-) diff --git a/app/Actions/Sharing/ListShare.php b/app/Actions/Sharing/ListShare.php index b4fd3123629..7247abf7891 100644 --- a/app/Actions/Sharing/ListShare.php +++ b/app/Actions/Sharing/ListShare.php @@ -12,17 +12,24 @@ class ListShare { /** - * @param User|null $user - * @param BaseAlbum|null $baseAlbum + * Returns a list of shares optionally filtered by the passed attributes. + * + * @param User|null $participant the optional user who participates + * in a sharing, i.e. the user with + * whom albums are shared + * @param User|null $owner the optional owner of the albums + * which are shared + * @param BaseAlbum|null $baseAlbum the optional album which is shared * * @return Shares * * @throws QueryBuilderException */ - public function do(?User $user, ?BaseAlbum $baseAlbum): Shares + public function do(?User $participant, ?User $owner, ?BaseAlbum $baseAlbum): Shares { try { - // prepare query + // Active shares, optionally filtered by album ID, participant ID + // and or owner ID $shared_query = DB::table('user_base_album') ->select([ 'user_base_album.id', @@ -33,27 +40,32 @@ public function do(?User $user, ?BaseAlbum $baseAlbum): Shares ]) ->join('users', 'user_id', '=', 'users.id') ->join('base_albums', 'base_album_id', '=', 'base_albums.id'); + if ($participant !== null) { + $shared_query->where('user_base_album.user_id', '=', $participant->id); + } + if ($owner !== null) { + $shared_query->where('base_albums.owner_id', '=', $owner->id); + } + if ($baseAlbum !== null) { + $shared_query->where('base_albums.id', '=', $baseAlbum->id); + } + $shared = $shared_query + ->orderBy('title', 'ASC') + ->orderBy('username', 'ASC') + ->get(); + // Existing albums which can be shared optionally filtered by + // album ID and/or owner ID $albums_query = DB::table('base_albums') ->leftJoin('albums', 'albums.id', '=', 'base_albums.id') ->select(['base_albums.id', 'title', 'parent_id']) ->orderBy('title', 'ASC'); - - // apply filter - if ($user !== null) { - $shared_query->where('base_albums.owner_id', '=', $user->id); - $albums_query->where('owner_id', '=', $user->id); + if ($owner !== null) { + $albums_query->where('owner_id', '=', $owner->id); } if ($baseAlbum !== null) { - $shared_query->where('base_albums.id', '=', $baseAlbum->id); $albums_query->where('base_albums.id', '=', $baseAlbum->id); } - - // get arrays - $shared = $shared_query - ->orderBy('title', 'ASC') - ->orderBy('username', 'ASC') - ->get(); $albums = $albums_query->get(); $this->linkAlbums($albums); $albums->each(function ($album) { @@ -64,10 +76,15 @@ public function do(?User $user, ?BaseAlbum $baseAlbum): Shares unset($album->parent); }); - $users = DB::table('users') - ->select(['id', 'username']) - ->where('id', '>', 0) - ->orderBy('username', 'ASC') + // Existing users with whom an album can be shared optionally + // filtered by participant ID + $users_query = DB::table('users')->select(['id', 'username']); + if ($participant !== null) { + $users_query->where('id', '=', $participant->id); + } else { + $users_query->where('id', '>', 0); + } + $users = $users_query->orderBy('username', 'ASC') ->get() ->each(function ($user) { $user->id = intval($user->id); diff --git a/app/Http/Controllers/Administration/SharingController.php b/app/Http/Controllers/Administration/SharingController.php index 17d7ee1de8c..9c8901d84ff 100644 --- a/app/Http/Controllers/Administration/SharingController.php +++ b/app/Http/Controllers/Administration/SharingController.php @@ -28,7 +28,7 @@ class SharingController extends Controller */ public function list(ListSharingRequest $request, ListShare $listShare): Shares { - return $listShare->do($request->user2(), $request->album()); + return $listShare->do($request->participant(), $request->owner(), $request->album()); } /** diff --git a/app/Http/Requests/Sharing/ListSharingRequest.php b/app/Http/Requests/Sharing/ListSharingRequest.php index 240729e8905..76bf96488d7 100644 --- a/app/Http/Requests/Sharing/ListSharingRequest.php +++ b/app/Http/Requests/Sharing/ListSharingRequest.php @@ -5,9 +5,7 @@ use App\Http\Requests\BaseApiRequest; use App\Http\Requests\Contracts\HasAbstractAlbum; use App\Http\Requests\Contracts\HasBaseAlbum; -use App\Http\Requests\Contracts\HasOptionalUser; use App\Http\Requests\Traits\HasBaseAlbumTrait; -use App\Http\Requests\Traits\HasOptionalUserTrait; use App\Models\User; use App\Policies\AlbumPolicy; use App\Policies\UserPolicy; @@ -19,19 +17,33 @@ /** * Represents a request for listing shares. * - * The result can be filtered by a specific album or user if the respective - * ID is included in the request. + * The result can be filtered by + * - a specific album via `albumID` + * - a specific user with whom the something is shared via `participantID`, or + * - a specific user who owns the albums which are shared via `ownerID` + * if the respective ID is included in the request. * * Non-admin user must only query for shares of albums they own or for * all shares they participate in. - * In other words, non-admin user must include at least their own user ID or - * an album ID they own in the request. + * In other words, non-admin user must include at least their own user ID as + * user ID or owner ID or an album ID they own in the request. * Only the admin is allowed to make an unrestricted query. */ -class ListSharingRequest extends BaseApiRequest implements HasBaseAlbum, HasOptionalUser +class ListSharingRequest extends BaseApiRequest implements HasBaseAlbum { use HasBaseAlbumTrait; - use HasOptionalUserTrait; + public const OWNER_ID_ATTRIBUTE = 'ownerID'; + public const PARTICIPANT_ID_ATTRIBUTE = 'participantID'; + + /** + * @var User|null + */ + protected ?User $owner; + + /** + * @var User|null + */ + protected ?User $participant; /** * {@inheritDoc} @@ -50,7 +62,10 @@ public function authorize(): bool return true; } - if ($this->user2 !== null && $this->user2->id === Auth::id()) { + if ( + ($this->owner !== null && $this->owner->id === Auth::id()) || + ($this->participant !== null && $this->participant->id === Auth::id()) + ) { return true; } @@ -64,7 +79,8 @@ public function rules(): array { return [ HasAbstractAlbum::ALBUM_ID_ATTRIBUTE => ['sometimes', new RandomIDRule(false)], - HasOptionalUser::USER_ID_ATTRIBUTE => ['sometimes', new IntegerIDRule(false)], + self::OWNER_ID_ATTRIBUTE => ['sometimes', new IntegerIDRule(false)], + self::PARTICIPANT_ID_ATTRIBUTE => ['sometimes', new IntegerIDRule(false)], ]; } @@ -76,8 +92,33 @@ protected function processValidatedValues(array $values, array $files): void $this->album = key_exists(HasAbstractAlbum::ALBUM_ID_ATTRIBUTE, $values) ? $this->albumFactory->findBaseAlbumOrFail($values[HasAbstractAlbum::ALBUM_ID_ATTRIBUTE]) : null; - $this->user2 = key_exists(HasOptionalUser::USER_ID_ATTRIBUTE, $values) ? - User::query()->find($values[HasOptionalUser::USER_ID_ATTRIBUTE]) : + $this->owner = key_exists(self::OWNER_ID_ATTRIBUTE, $values) ? + User::query()->findOrFail($values[self::OWNER_ID_ATTRIBUTE]) : + null; + $this->participant = key_exists(self::PARTICIPANT_ID_ATTRIBUTE, $values) ? + User::query()->findOrFail($values[self::PARTICIPANT_ID_ATTRIBUTE]) : null; } + + /** + * Returns the optional album owner to which the list of shares shall be + * restricted. + * + * @return User|null + */ + public function owner(): ?User + { + return $this->owner; + } + + /** + * Returns the optional share participant to which the list of shares + * shall be restricted. + * + * @return User|null + */ + public function participant(): ?User + { + return $this->participant; + } } diff --git a/public/Lychee-front b/public/Lychee-front index fbcff79af52..2cfbce02321 160000 --- a/public/Lychee-front +++ b/public/Lychee-front @@ -1 +1 @@ -Subproject commit fbcff79af52c3a5db014062a0b5cd04138f03ec6 +Subproject commit 2cfbce023218be0db21391de944d86c76eac0677 diff --git a/public/dist/main.js b/public/dist/main.js index 3d49fb49a71..e5fcf21d87e 100644 --- a/public/dist/main.js +++ b/public/dist/main.js @@ -2482,19 +2482,39 @@ album.qrCode = function () { return; } - basicModal.show({ - body: "
", - classList: ["qr-code"], - readyCB: function readyCB(formElements, dialog) { - var qrcode = dialog.querySelector("div.qr-code-canvas"); + // We need this indirection based on a resize observer, because the ready + // callback of the dialog is invoked _before_ the dialog is made visible + // in order to allow the ready callback to make initializations of + // form elements without causing flicker. + // However, for invisible elements `.clientWidth` returns zero, hence + // we cannot paint the QR code onto the canvas before it becomes visible. + var qrCodeCanvasObserver = function () { + var width = 0; + return new ResizeObserver(function (entries, observer) { + var qrCodeCanvas = entries[0].target; + // Avoid infinite resize events due to clearing and repainting + // the same QR code on the canvas. + if (width === qrCodeCanvas.clientWidth) { + return; + } + width = qrCodeCanvas.clientWidth; QrCreator.render({ text: location.href, radius: 0.0, ecLevel: "H", fill: "#000000", background: "#FFFFFF", - size: qrcode.clientWidth - }, qrcode); + size: width + }, qrCodeCanvas); + }); + }(); + + basicModal.show({ + body: "", + classList: ["qr-code"], + readyCB: function readyCB(formElements, dialog) { + var qrCodeCanvas = dialog.querySelector("canvas"); + qrCodeCanvasObserver.observe(qrCodeCanvas); }, buttons: { cancel: { @@ -9489,24 +9509,47 @@ _photo3.qrCode = function (photoID) { return; } - basicModal.show({ - body: "
", - classList: ["qr-code"], - readyCB: function readyCB(formElements, dialog) { - var qrcode = dialog.querySelector("div.qr-code-canvas"); + // We need this indirection based on a resize observer, because the ready + // callback of the dialog is invoked _before_ the dialog is made visible + // in order to allow the ready callback to make initializations of + // form elements without causing flicker. + // However, for invisible elements `.clientWidth` returns zero, hence + // we cannot paint the QR code onto the canvas before it becomes visible. + var qrCodeCanvasObserver = function () { + var width = 0; + return new ResizeObserver(function (entries, observer) { + var qrCodeCanvas = entries[0].target; + // Avoid infinite resize events due to clearing and repainting + // the same QR code on the canvas. + if (width === qrCodeCanvas.clientWidth) { + return; + } + width = qrCodeCanvas.clientWidth; QrCreator.render({ text: _photo3.getViewLink(myPhoto.id), radius: 0.0, ecLevel: "H", fill: "#000000", background: "#FFFFFF", - size: qrcode.clientWidth - }, qrcode); + size: width + }, qrCodeCanvas); + }); + }(); + + basicModal.show({ + body: "", + classList: ["qr-code"], + readyCB: function readyCB(formElements, dialog) { + var qrCodeCanvas = dialog.querySelector("canvas"); + qrCodeCanvasObserver.observe(qrCodeCanvas); }, buttons: { cancel: { title: lychee.locale["CLOSE"], - fn: basicModal.close + fn: function fn() { + qrCodeCanvasObserver.disconnect(); + basicModal.close(); + } } } }); @@ -10485,10 +10528,19 @@ sharing.delete = function () { }; /** + * Queries the backend for a list of active shares, sharable albums and users + * with whom albums can be shared. + * + * For admin user, the query is unrestricted, for non-admin user the + * query is restricted to albums which are owned by the currently + * authenticated user. + * The latter is required as the backend forbids unrestricted queries for + * non-admin users. + * * @returns {void} */ sharing.list = function () { - api.post("Sharing::list", {}, + api.post("Sharing::list", lychee.rights.is_admin ? {} : { ownerID: lychee.user.id }, /** @param {SharingInfo} data */ function (data) { sharing.json = data;