Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Restrict IDs in v3 API using Range annotation #1161

Merged
merged 1 commit into from
Aug 12, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
65 changes: 15 additions & 50 deletions Shoko.Server/API/v3/Controllers/EpisodeController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -42,8 +42,6 @@ public class EpisodeController : BaseController

private readonly WatchedStatusService _watchedService;

internal const string EpisodeWithZeroID = "episodeID must be greater than 0";

internal const string EpisodeNotFoundWithEpisodeID = "No Episode entry for the given episodeID";

internal const string EpisodeNotFoundForAnidbEpisodeID = "No Episode entry for the given anidbEpisodeID";
Expand Down Expand Up @@ -308,16 +306,13 @@ public ActionResult<ListResult<Episode>> GetAllEpisodes(
/// <returns></returns>
[HttpGet("{episodeID}")]
public ActionResult<Episode> GetEpisodeByEpisodeID(
[FromRoute] int episodeID,
[FromRoute, Range(1, int.MaxValue)] int episodeID,
[FromQuery] bool includeFiles = false,
[FromQuery] bool includeMediaInfo = false,
[FromQuery] bool includeAbsolutePaths = false,
[FromQuery] bool includeXRefs = false,
[FromQuery, ModelBinder(typeof(CommaDelimitedModelBinder))] HashSet<DataSource> includeDataFrom = null)
{
if (episodeID == 0)
return BadRequest(EpisodeWithZeroID);

var episode = RepoFactory.AnimeEpisode.GetByID(episodeID);
if (episode == null)
return NotFound(EpisodeNotFoundWithEpisodeID);
Expand All @@ -340,11 +335,8 @@ public ActionResult<Episode> GetEpisodeByEpisodeID(
/// <returns></returns>
[Authorize("admin")]
[HttpPost("{episodeID}/OverrideTitle")]
public ActionResult OverrideEpisodeTitle([FromRoute] int episodeID, [FromBody(EmptyBodyBehavior = EmptyBodyBehavior.Disallow)] Episode.Input.EpisodeTitleOverrideBody body)
public ActionResult OverrideEpisodeTitle([FromRoute, Range(1, int.MaxValue)] int episodeID, [FromBody(EmptyBodyBehavior = EmptyBodyBehavior.Disallow)] Episode.Input.EpisodeTitleOverrideBody body)
{
if (episodeID == 0)
return BadRequest(EpisodeWithZeroID);

var episode = RepoFactory.AnimeEpisode.GetByID(episodeID);

if (episode == null)
Expand Down Expand Up @@ -378,11 +370,8 @@ public ActionResult OverrideEpisodeTitle([FromRoute] int episodeID, [FromBody(Em
/// <returns></returns>
[Authorize("admin")]
[HttpPost("{episodeID}/SetHidden")]
public ActionResult PostEpisodeSetHidden([FromRoute] int episodeID, [FromQuery] bool value = true, [FromQuery] bool updateStats = true)
public ActionResult PostEpisodeSetHidden([FromRoute, Range(1, int.MaxValue)] int episodeID, [FromQuery] bool value = true, [FromQuery] bool updateStats = true)
{
if (episodeID == 0)
return BadRequest(EpisodeWithZeroID);

var episode = RepoFactory.AnimeEpisode.GetByID(episodeID);
if (episode == null)
return NotFound(EpisodeNotFoundWithEpisodeID);
Expand Down Expand Up @@ -422,11 +411,8 @@ public ActionResult PostEpisodeSetHidden([FromRoute] int episodeID, [FromQuery]
/// <param name="episodeID">Shoko ID</param>
/// <returns></returns>
[HttpGet("{episodeID}/AniDB")]
public ActionResult<Episode.AniDB> GetEpisodeAnidbByEpisodeID([FromRoute] int episodeID)
public ActionResult<Episode.AniDB> GetEpisodeAnidbByEpisodeID([FromRoute, Range(1, int.MaxValue)] int episodeID)
{
if (episodeID == 0)
return BadRequest(EpisodeWithZeroID);

var episode = RepoFactory.AnimeEpisode.GetByID(episodeID);
if (episode == null)
return NotFound(EpisodeNotFoundWithEpisodeID);
Expand Down Expand Up @@ -490,11 +476,8 @@ public ActionResult<Episode> GetEpisode(
/// <param name="vote"></param>
/// <returns></returns>
[HttpPost("{episodeID}/Vote")]
public ActionResult PostEpisodeVote([FromRoute] int episodeID, [FromBody] Vote vote)
public ActionResult PostEpisodeVote([FromRoute, Range(1, int.MaxValue)] int episodeID, [FromBody] Vote vote)
{
if (episodeID == 0)
return BadRequest(EpisodeWithZeroID);

var episode = RepoFactory.AnimeEpisode.GetByID(episodeID);
if (episode == null)
return NotFound(EpisodeNotFoundWithEpisodeID);
Expand Down Expand Up @@ -527,7 +510,7 @@ public ActionResult PostEpisodeVote([FromRoute] int episodeID, [FromBody] Vote v
/// <returns>All TMDB Movies linked directly to the Shoko Episode.</returns>
[HttpGet("{episodeID}/TMDB/Movie")]
public ActionResult<List<TmdbMovie>> GetTmdbMoviesByEpisodeID(
[FromRoute] int episodeID,
[FromRoute, Range(1, int.MaxValue)] int episodeID,
[FromQuery, ModelBinder(typeof(CommaDelimitedModelBinder))] HashSet<TmdbMovie.IncludeDetails> include = null,
[FromQuery, ModelBinder(typeof(CommaDelimitedModelBinder))] HashSet<TitleLanguage> language = null
)
Expand All @@ -550,7 +533,7 @@ public ActionResult<List<TmdbMovie>> GetTmdbMoviesByEpisodeID(
/// <returns>All TMDB Movie cross-references for the Shoko Episode.</returns>
[HttpGet("{seriesID}/TMDB/Movie/CrossReferences")]
public ActionResult<IReadOnlyList<TmdbMovie.CrossReference>> GetTMDBMovieCrossReferenceByEpisodeID(
[FromRoute] int seriesID
[FromRoute, Range(1, int.MaxValue)] int seriesID
)
{
var episode = RepoFactory.AnimeEpisode.GetByID(seriesID);
Expand All @@ -572,7 +555,7 @@ [FromRoute] int seriesID
/// <returns>All TMDB Episodes linked to the Shoko Episode.</returns>
[HttpGet("{episodeID}/TMDB/Episode")]
public ActionResult<List<TmdbEpisode>> GetTmdbEpisodesByEpisodeID(
[FromRoute] int episodeID,
[FromRoute, Range(1, int.MaxValue)] int episodeID,
[FromQuery, ModelBinder(typeof(CommaDelimitedModelBinder))] HashSet<TmdbEpisode.IncludeDetails> include = null,
[FromQuery, ModelBinder(typeof(CommaDelimitedModelBinder))] HashSet<TitleLanguage> language = null
)
Expand All @@ -595,7 +578,7 @@ public ActionResult<List<TmdbEpisode>> GetTmdbEpisodesByEpisodeID(
/// <returns>All TMDB Episode cross-references for the Shoko Episode.</returns>
[HttpGet("{seriesID}/TMDB/Episode/CrossReferences")]
public ActionResult<IReadOnlyList<TmdbEpisode.CrossReference>> GetTMDBEpisodeCrossReferenceByEpisodeID(
[FromRoute] int seriesID
[FromRoute, Range(1, int.MaxValue)] int seriesID
)
{
var episode = RepoFactory.AnimeEpisode.GetByID(seriesID);
Expand All @@ -618,11 +601,8 @@ [FromRoute] int seriesID
/// <param name="episodeID">Shoko ID</param>
/// <returns></returns>
[HttpGet("{episodeID}/TvDB")]
public ActionResult<List<Episode.TvDB>> GetEpisodeTvDBDetails([FromRoute] int episodeID)
public ActionResult<List<Episode.TvDB>> GetEpisodeTvDBDetails([FromRoute, Range(1, int.MaxValue)] int episodeID)
{
if (episodeID == 0)
return BadRequest(EpisodeWithZeroID);

var episode = RepoFactory.AnimeEpisode.GetByID(episodeID);
if (episode == null)
return NotFound(EpisodeNotFoundWithEpisodeID);
Expand Down Expand Up @@ -658,11 +638,8 @@ [FromRoute] int seriesID
/// <param name="includeDisabled"></param>
/// <returns></returns>
[HttpGet("{episodeID}/Images")]
public ActionResult<Images> GetSeriesImages([FromRoute] int episodeID, [FromQuery] bool includeDisabled)
public ActionResult<Images> GetSeriesImages([FromRoute, Range(1, int.MaxValue)] int episodeID, [FromQuery] bool includeDisabled)
{
if (episodeID == 0)
return BadRequest(EpisodeWithZeroID);

var episode = RepoFactory.AnimeEpisode.GetByID(episodeID);
if (episode == null)
return NotFound(EpisodeNotFoundWithEpisodeID);
Expand All @@ -688,15 +665,12 @@ public ActionResult<Images> GetSeriesImages([FromRoute] int episodeID, [FromQuer
/// <param name="imageType">Poster, Banner, Fanart</param>
/// <returns></returns>
[HttpGet("{episodeID}/Images/{imageType}")]
public ActionResult<Image> GetSeriesDefaultImageForType([FromRoute] int episodeID,
public ActionResult<Image> GetSeriesDefaultImageForType([FromRoute, Range(1, int.MaxValue)] int episodeID,
[FromRoute] Image.ImageType imageType)
{
if (!_allowedImageTypes.Contains(imageType))
return NotFound();

if (episodeID == 0)
return BadRequest(EpisodeWithZeroID);

var episode = RepoFactory.AnimeEpisode.GetByID(episodeID);
if (episode == null)
return NotFound(EpisodeNotFoundWithEpisodeID);
Expand Down Expand Up @@ -733,15 +707,12 @@ public ActionResult<Image> GetSeriesDefaultImageForType([FromRoute] int episodeI
/// <param name="body">The body containing the source and id used to set.</param>
/// <returns></returns>
[HttpPut("{episodeID}/Images/{imageType}")]
public ActionResult<Image> SetSeriesDefaultImageForType([FromRoute] int episodeID,
public ActionResult<Image> SetSeriesDefaultImageForType([FromRoute, Range(1, int.MaxValue)] int episodeID,
[FromRoute] Image.ImageType imageType, [FromBody] Image.Input.DefaultImageBody body)
{
if (!_allowedImageTypes.Contains(imageType))
return NotFound();

if (episodeID == 0)
return BadRequest(EpisodeWithZeroID);

var episode = RepoFactory.AnimeEpisode.GetByID(episodeID);
if (episode == null)
return NotFound(EpisodeNotFoundWithEpisodeID);
Expand Down Expand Up @@ -779,14 +750,11 @@ public ActionResult<Image> SetSeriesDefaultImageForType([FromRoute] int episodeI
/// <param name="imageType">Poster, Banner, Fanart</param>
/// <returns></returns>
[HttpDelete("{episodeID}/Images/{imageType}")]
public ActionResult DeleteSeriesDefaultImageForType([FromRoute] int episodeID, [FromRoute] Image.ImageType imageType)
public ActionResult DeleteSeriesDefaultImageForType([FromRoute, Range(1, int.MaxValue)] int episodeID, [FromRoute] Image.ImageType imageType)
{
if (!_allowedImageTypes.Contains(imageType))
return NotFound();

if (episodeID == 0)
return BadRequest(EpisodeWithZeroID);

var episode = RepoFactory.AnimeEpisode.GetByID(episodeID);
if (episode == null)
return NotFound(EpisodeNotFoundWithEpisodeID);
Expand Down Expand Up @@ -822,11 +790,8 @@ public ActionResult DeleteSeriesDefaultImageForType([FromRoute] int episodeID, [
/// <param name="watched"></param>
/// <returns></returns>
[HttpPost("{episodeID}/Watched/{watched}")]
public async Task<ActionResult> SetWatchedStatusOnEpisode([FromRoute] int episodeID, [FromRoute] bool watched)
public async Task<ActionResult> SetWatchedStatusOnEpisode([FromRoute, Range(1, int.MaxValue)] int episodeID, [FromRoute] bool watched)
{
if (episodeID == 0)
return BadRequest(EpisodeWithZeroID);

var episode = RepoFactory.AnimeEpisode.GetByID(episodeID);
if (episode == null)
return NotFound(EpisodeNotFoundWithEpisodeID);
Expand Down
14 changes: 6 additions & 8 deletions Shoko.Server/API/v3/Controllers/FilterController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -213,7 +213,7 @@ public ActionResult<Filter> AddNewFilter(Filter.Input.CreateOrUpdateFilterBody b
/// <param name="withConditions">Include conditions and sort criteria in the response.</param>
/// <returns>The filter</returns>
[HttpGet("{filterID}")]
public ActionResult<Filter> GetFilter([FromRoute] int filterID, [FromQuery] bool withConditions = false)
public ActionResult<Filter> GetFilter([FromRoute, Range(1, int.MaxValue)] int filterID, [FromQuery] bool withConditions = false)
{
var filterPreset = RepoFactory.FilterPreset.GetByID(filterID);
if (filterPreset == null)
Expand All @@ -231,7 +231,7 @@ public ActionResult<Filter> GetFilter([FromRoute] int filterID, [FromQuery] bool
/// <returns>The updated filter.</returns>
[Authorize("admin")]
[HttpPatch("{filterID}")]
public ActionResult<Filter> PatchFilter([FromRoute] int filterID, JsonPatchDocument<Filter.Input.CreateOrUpdateFilterBody> document)
public ActionResult<Filter> PatchFilter([FromRoute, Range(1, int.MaxValue)] int filterID, JsonPatchDocument<Filter.Input.CreateOrUpdateFilterBody> document)
{
var filterPreset = RepoFactory.FilterPreset.GetByID(filterID);
if (filterPreset == null)
Expand Down Expand Up @@ -265,7 +265,7 @@ public ActionResult<Filter> PatchFilter([FromRoute] int filterID, JsonPatchDocum
/// <returns>The updated filter.</returns>
[Authorize("admin")]
[HttpPut("{filterID}")]
public ActionResult<Filter> PutFilter([FromRoute] int filterID, Filter.Input.CreateOrUpdateFilterBody body)
public ActionResult<Filter> PutFilter([FromRoute, Range(1, int.MaxValue)] int filterID, Filter.Input.CreateOrUpdateFilterBody body)
{
var filterPreset = RepoFactory.FilterPreset.GetByID(filterID);
if (filterPreset == null)
Expand Down Expand Up @@ -293,7 +293,7 @@ public ActionResult<Filter> PutFilter([FromRoute] int filterID, Filter.Input.Cre
/// <returns>Void.</returns>
[Authorize("admin")]
[HttpDelete("{filterID}")]
public ActionResult DeleteFilter(int filterID)
public ActionResult DeleteFilter([FromRoute, Range(1, int.MaxValue)] int filterID)
{
var filterPreset = RepoFactory.FilterPreset.GetByID(filterID);
if (filterPreset == null)
Expand Down Expand Up @@ -433,14 +433,13 @@ public ActionResult<ListResult<Series>> GetPreviewSeriesInFilteredGroup([FromBod
/// <param name="includeEmpty">Include <see cref="Series"/> with missing <see cref="Episode"/>s in the search.</param>
/// <returns></returns>
[HttpPost("Preview/Group/{groupID}/Group")]
public ActionResult<List<Group>> GetPreviewFilteredSubGroups([FromBody] Filter.Input.CreateOrUpdateFilterBody filter, [FromRoute] int groupID,
public ActionResult<List<Group>> GetPreviewFilteredSubGroups([FromBody] Filter.Input.CreateOrUpdateFilterBody filter, [FromRoute, Range(1, int.MaxValue)] int groupID,
[FromQuery] bool randomImages = false, [FromQuery] bool includeEmpty = false)
{
var filterPreset = _factory.GetFilterPreset(filter, ModelState);
if (!ModelState.IsValid) return ValidationProblem(ModelState);

// Check if the group exists.
if (groupID == 0) return BadRequest(GroupController.GroupWithZeroID);
var group = RepoFactory.AnimeGroup.GetByID(groupID);
if (group == null)
return NotFound(GroupController.GroupNotFound);
Expand Down Expand Up @@ -493,15 +492,14 @@ public ActionResult<List<Group>> GetPreviewFilteredSubGroups([FromBody] Filter.I
/// <param name="includeDataFrom">Include data from selected <see cref="DataSource"/>s.</param>
/// /// <returns></returns>
[HttpPost("Preview/Group/{groupID}/Series")]
public ActionResult<List<Series>> GetPreviewSeriesInFilteredGroup([FromBody] Filter.Input.CreateOrUpdateFilterBody filter, [FromRoute] int groupID,
public ActionResult<List<Series>> GetPreviewSeriesInFilteredGroup([FromBody] Filter.Input.CreateOrUpdateFilterBody filter, [FromRoute, Range(1, int.MaxValue)] int groupID,
[FromQuery] bool recursive = false, [FromQuery] bool includeMissing = false,
[FromQuery] bool randomImages = false, [FromQuery, ModelBinder(typeof(CommaDelimitedModelBinder))] HashSet<DataSource> includeDataFrom = null)
{
var filterPreset = _factory.GetFilterPreset(filter, ModelState);
if (!ModelState.IsValid) return ValidationProblem(ModelState);

// Check if the group exists.
if (groupID == 0) return BadRequest(GroupController.GroupWithZeroID);
var group = RepoFactory.AnimeGroup.GetByID(groupID);
if (group == null)
return NotFound(GroupController.GroupNotFound);
Expand Down
20 changes: 6 additions & 14 deletions Shoko.Server/API/v3/Controllers/GroupController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -30,8 +30,6 @@ public class GroupController : BaseController

#region Return messages

internal const string GroupWithZeroID = "GroupID must be greater than 0";

internal const string GroupNotFound = "No Group entry for the given groupID";

internal const string GroupForbiddenForUser = "Accessing Group is not allowed for the current user";
Expand Down Expand Up @@ -162,9 +160,8 @@ public ActionResult<Group> CreateGroup([FromBody] Group.Input.CreateOrUpdateGrou
/// <param name="groupID"></param>
/// <returns></returns>
[HttpGet("{groupID}")]
public ActionResult<Group> GetGroup([FromRoute] int groupID)
public ActionResult<Group> GetGroup([FromRoute, Range(1, int.MaxValue)] int groupID)
{
if (groupID == 0) return BadRequest(GroupWithZeroID);
var group = RepoFactory.AnimeGroup.GetByID(groupID);
if (group == null)
{
Expand All @@ -190,9 +187,8 @@ public ActionResult<Group> GetGroup([FromRoute] int groupID)
/// <param name="body">The new details for the group.</param>
/// <returns>The updated group.</returns>
[HttpPut("{groupID}")]
public ActionResult<Group> PutGroup([FromRoute] int groupID, [FromBody] Group.Input.CreateOrUpdateGroupBody body)
public ActionResult<Group> PutGroup([FromRoute, Range(1, int.MaxValue)] int groupID, [FromBody] Group.Input.CreateOrUpdateGroupBody body)
{
if (groupID == 0) return BadRequest(GroupWithZeroID);
var animeGroup = RepoFactory.AnimeGroup.GetByID(groupID);
if (animeGroup == null)
{
Expand Down Expand Up @@ -226,9 +222,8 @@ public ActionResult<Group> PutGroup([FromRoute] int groupID, [FromBody] Group.In
/// <param name="patchDocument">The JSON Patch document containing the changes to be applied to the group.</param>
/// <returns>The updated group.</returns>
[HttpPatch("{groupID}")]
public ActionResult<Group> PatchGroup([FromRoute] int groupID, [FromBody] JsonPatchDocument<Group.Input.CreateOrUpdateGroupBody> patchDocument)
public ActionResult<Group> PatchGroup([FromRoute, Range(1, int.MaxValue)] int groupID, [FromBody] JsonPatchDocument<Group.Input.CreateOrUpdateGroupBody> patchDocument)
{
if (groupID == 0) return BadRequest(GroupWithZeroID);
var animeGroup = RepoFactory.AnimeGroup.GetByID(groupID);
if (animeGroup == null)
{
Expand Down Expand Up @@ -268,10 +263,9 @@ public ActionResult<Group> PatchGroup([FromRoute] int groupID, [FromBody] JsonPa
/// <param name="recursive">Show relations for all series within the group, even for series within sub-groups.</param>
/// <returns></returns>
[HttpGet("{groupID}/Relations")]
public ActionResult<List<SeriesRelation>> GetShokoRelationsBySeriesID([FromRoute] int groupID,
public ActionResult<List<SeriesRelation>> GetShokoRelationsBySeriesID([FromRoute, Range(1, int.MaxValue)] int groupID,
[FromQuery] bool recursive = false)
{
if (groupID == 0) return BadRequest(GroupWithZeroID);
var group = RepoFactory.AnimeGroup.GetByID(groupID);
if (group == null)
{
Expand Down Expand Up @@ -315,9 +309,8 @@ public ActionResult<List<SeriesRelation>> GetShokoRelationsBySeriesID([FromRoute
/// <returns></returns>
[Authorize("admin")]
[HttpDelete("{groupID}")]
public async Task<ActionResult> DeleteGroup(int groupID, bool deleteSeries = false, bool deleteFiles = false)
public async Task<ActionResult> DeleteGroup([FromRoute, Range(1, int.MaxValue)] int groupID, bool deleteSeries = false, bool deleteFiles = false)
{
if (groupID == 0) return BadRequest(GroupWithZeroID);
var group = RepoFactory.AnimeGroup.GetByID(groupID);
if (group == null)
{
Expand Down Expand Up @@ -351,9 +344,8 @@ public async Task<ActionResult> DeleteGroup(int groupID, bool deleteSeries = fal
/// <param name="groupID"></param>
/// <returns></returns>
[HttpPost("{groupID}/Recalculate")]
public async Task<ActionResult> RecalculateStats(int groupID)
public async Task<ActionResult> RecalculateStats([FromRoute, Range(1, int.MaxValue)] int groupID)
{
if (groupID == 0) return BadRequest(GroupWithZeroID);
var group = RepoFactory.AnimeGroup.GetByID(groupID);
if (group == null)
{
Expand Down
Loading