fix: only consider episodes accessible if subscribed to podcasts

This commit is contained in:
Phan An 2024-06-04 11:44:33 +02:00
parent ee4c97168b
commit 0b622ba0d7
7 changed files with 58 additions and 14 deletions

View file

@ -69,17 +69,28 @@ class SongBuilder extends Builder
); );
} }
public function accessibleBy(User $user, bool $withTableName = true): self public function accessibleBy(User $user): self
{ {
if (License::isCommunity()) { if (License::isCommunity()) {
// In the Community Edition, all songs are accessible by all users. // In the Community Edition, all songs are accessible by all users.
return $this; return $this;
} }
return $this->where(static function (Builder $query) use ($user, $withTableName): void { // We want to alias both podcasts and podcast_user tables to avoid possible conflicts with other joins.
$query->where(($withTableName ? 'songs.' : '') . 'is_public', true) return $this->leftJoin('podcasts as podcasts_a11y', 'songs.podcast_id', 'podcasts_a11y.id')
->orWhere(($withTableName ? 'songs.' : '') . 'owner_id', $user->id); ->leftJoin('podcast_user as podcast_user_a11y', static function (JoinClause $join) use ($user): void {
}); $join->on('podcasts_a11y.id', 'podcast_user_a11y.podcast_id')
->where('podcast_user_a11y.user_id', $user->id);
})
->where(static function (Builder $query) use ($user): void {
// Songs must be public or owned by the user.
$query->where('songs.is_public', true)
->orWhere('songs.owner_id', $user->id);
})->whereNot(static function (Builder $query): void {
// Episodes must belong to a podcast that the user is not subscribed to.
$query->whereNotNull('songs.podcast_id')
->whereNull('podcast_user_a11y.podcast_id');
});
} }
private function sortByOneColumn(string $column, string $direction): self private function sortByOneColumn(string $column, string $direction): self

View file

@ -19,6 +19,10 @@ enum SmartPlaylistModel: string
public function toColumnName(): string public function toColumnName(): string
{ {
return match ($this) { return match ($this) {
self::TITLE => 'songs.title',
self::LENGTH => 'songs.length',
self::GENRE => 'songs.genre',
self::YEAR => 'songs.year',
self::ALBUM_NAME => 'albums.name', self::ALBUM_NAME => 'albums.name',
self::ARTIST_NAME => 'artists.name', self::ARTIST_NAME => 'artists.name',
self::DATE_ADDED => 'songs.created_at', self::DATE_ADDED => 'songs.created_at',

View file

@ -47,10 +47,10 @@ class PlaylistSongController extends Controller
$this->authorize('collaborate', $playlist); $this->authorize('collaborate', $playlist);
$songs = $this->songRepository->getMany(ids: $request->songs, scopedUser: $this->user); $playables = $this->songRepository->getMany(ids: $request->songs, scopedUser: $this->user);
return self::createResourceCollection( return self::createResourceCollection(
$this->playlistService->addPlayablesToPlaylist($playlist, $songs, $this->user) $this->playlistService->addPlayablesToPlaylist($playlist, $playables, $this->user)
); );
} }

View file

@ -2,19 +2,18 @@
namespace App\Http\Requests\API; namespace App\Http\Requests\API;
use App\Models\Song; use App\Rules\AllPlayablesAreAccessibleBy;
use Illuminate\Validation\Rule;
/** /**
* @property-read array<string> $songs * @property-read array<string> $songs
*/ */
class AddSongsToPlaylistRequest extends Request class AddSongsToPlaylistRequest extends Request
{ {
/** @return array<mixed> */ /** @inheritdoc */
public function rules(): array public function rules(): array
{ {
return [ return [
'songs' => ['required', 'array', Rule::exists(Song::class, 'id')], 'songs' => ['required', 'array', new AllPlayablesAreAccessibleBy($this->user())],
]; ];
} }
} }

View file

@ -3,7 +3,7 @@
namespace App\Http\Requests\API; namespace App\Http\Requests\API;
use App\Models\PlaylistFolder; use App\Models\PlaylistFolder;
use App\Models\Song; use App\Rules\AllPlayablesAreAccessibleBy;
use App\Rules\ValidSmartPlaylistRulePayload; use App\Rules\ValidSmartPlaylistRulePayload;
use Illuminate\Validation\Rule; use Illuminate\Validation\Rule;
@ -21,8 +21,7 @@ class PlaylistStoreRequest extends Request
{ {
return [ return [
'name' => 'required', 'name' => 'required',
'songs' => 'array', 'songs' => ['array', new AllPlayablesAreAccessibleBy($this->user())],
'songs.*' => [Rule::exists(Song::class, 'id')],
'rules' => ['array', 'nullable', new ValidSmartPlaylistRulePayload()], 'rules' => ['array', 'nullable', new ValidSmartPlaylistRulePayload()],
'folder_id' => ['nullable', 'sometimes', Rule::exists(PlaylistFolder::class, 'id')], 'folder_id' => ['nullable', 'sometimes', Rule::exists(PlaylistFolder::class, 'id')],
'own_songs_only' => 'sometimes', 'own_songs_only' => 'sometimes',

View file

@ -54,6 +54,7 @@ use Throwable;
* // The following are only available for collaborative playlists * // The following are only available for collaborative playlists
* @property-read ?string $collaborator_email The email of the user who added the song to the playlist * @property-read ?string $collaborator_email The email of the user who added the song to the playlist
* @property-read ?string $collaborator_name The name of the user who added the song to the playlist * @property-read ?string $collaborator_name The name of the user who added the song to the playlist
* @property-read ?string $collaborator_avatar The avatar of the user who added the song to the playlist
* @property-read ?int $collaborator_id The ID of the user who added the song to the playlist * @property-read ?int $collaborator_id The ID of the user who added the song to the playlist
* @property-read ?string $added_at The date the song was added to the playlist * @property-read ?string $added_at The date the song was added to the playlist
* @property-read PlayableType $type * @property-read PlayableType $type

View file

@ -0,0 +1,30 @@
<?php
namespace App\Rules;
use App\Models\User;
use App\Repositories\SongRepository;
use Closure;
use Illuminate\Contracts\Validation\ValidationRule;
use Illuminate\Support\Arr;
class AllPlayablesAreAccessibleBy implements ValidationRule
{
public function __construct(private readonly User $user)
{
}
/**
* Run the validation rule.
*
* @param array<string> $value
*/
public function validate(string $attribute, mixed $value, Closure $fail): void
{
$ids = array_unique(Arr::wrap($value));
if ($ids && app(SongRepository::class)->getMany(ids: $ids, scopedUser: $this->user)->count() !== count($ids)) {
$fail('Not all songs or episodes exist in the database or are accessible by the user.');
}
}
}