Skip to content

Commit

Permalink
Merge branch 'main' into add-subform-layoutset
Browse files Browse the repository at this point in the history
  • Loading branch information
framitdavid authored Sep 27, 2024
2 parents 6e5972a + 1bce444 commit 6a6a130
Show file tree
Hide file tree
Showing 161 changed files with 11,102 additions and 1,859 deletions.
155 changes: 155 additions & 0 deletions backend/src/Designer/Controllers/ImageController.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,155 @@
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using System.Web;
using Altinn.Studio.Designer.Enums;
using Altinn.Studio.Designer.Exceptions.AppDevelopment;
using Altinn.Studio.Designer.Helpers;
using Altinn.Studio.Designer.Models;
using Altinn.Studio.Designer.Services.Interfaces;
using Altinn.Studio.Designer.TypedHttpClients.ImageClient;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Primitives;
using Microsoft.Net.Http.Headers;

namespace Altinn.Studio.Designer.Controllers;

/// <summary>
/// Controller containing actions related to images
/// </summary>
[Authorize]
[AutoValidateAntiforgeryToken]
[Route("designer/api/{org}/{app:regex(^(?!datamodels$)[[a-z]][[a-z0-9-]]{{1,28}}[[a-z0-9]]$)}/images")]
public class ImageController : ControllerBase
{

private readonly IImagesService _imagesService;
private readonly ImageClient _imageClient;

/// <summary>
/// Initializes a new instance of the <see cref="ImageController"/> class.
/// </summary>
/// <param name="imagesService">The images service.</param>
/// <param name="imageClient">A http client to validate external image url</param>
public ImageController(IImagesService imagesService, ImageClient imageClient)
{
_imagesService = imagesService;
_imageClient = imageClient;
}

/// <summary>
/// Endpoint for getting a specific image
/// </summary>
/// <param name="org">Unique identifier of the organisation responsible for the app.</param>
/// <param name="app">Application identifier which is unique within an organisation.</param>
/// <param name="encodedImagePath">Relative encoded path of image to fetch</param>
/// <returns>Image</returns>
[HttpGet("{encodedImagePath}")]
[ProducesResponseType(StatusCodes.Status200OK)]
public FileStreamResult GetImageByName(string org, string app, [FromRoute] string encodedImagePath)
{
string developer = AuthenticationHelper.GetDeveloperUserName(HttpContext);
string decodedImagePath = HttpUtility.UrlDecode(encodedImagePath);
var editingContext = AltinnRepoEditingContext.FromOrgRepoDeveloper(org, app, developer);

return _imagesService.GetImage(editingContext, decodedImagePath);
}

/// <summary>
/// Endpoint for getting all image file names in application
/// </summary>
/// <param name="org">Unique identifier of the organisation responsible for the app.</param>
/// <param name="app">Application identifier which is unique within an organisation.</param>
/// <returns>All image file names</returns>
[HttpGet("fileNames")]
[ProducesResponseType(StatusCodes.Status200OK)]
public ActionResult<List<string>> GetAllImagesFileNames(string org, string app)
{
string developer = AuthenticationHelper.GetDeveloperUserName(HttpContext);
var editingContext = AltinnRepoEditingContext.FromOrgRepoDeveloper(org, app, developer);

List<string> imageFileNames = _imagesService.GetAllImageFileNames(editingContext);

return Ok(imageFileNames);
}

/// <summary>
/// Endpoint to validate a given url for fetching an external image.
/// </summary>
/// <param name="url">An external url to fetch an image to represent in the image component in the form.</param>
/// <returns>NotAnImage if url does not point at an image or NotValidUrl if url is invalid for any other reason</returns>
[HttpGet("validate")]
[ProducesResponseType(StatusCodes.Status200OK)]
public async Task<ImageUrlValidationResult> ValidateExternalImageUrl([FromQuery] string url)
{
return await _imageClient.ValidateUrlAsync(url);
}

/// <summary>
/// Endpoint for uploading image to application.
/// </summary>
/// <param name="org">Unique identifier of the organisation responsible for the app.</param>
/// <param name="app">Application identifier which is unique within an organisation.</param>
/// <param name="image">The actual image</param>
/// <param name="overrideExisting">Optional parameter that overrides existing image if set. Default is false</param>
[HttpPost]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
public async Task<ActionResult> UploadImage(string org, string app, [FromForm(Name = "file")] IFormFile image, [FromForm(Name = "overrideExisting")] bool overrideExisting = false)
{
if (image == null || image.Length == 0)
{
return BadRequest("No file uploaded.");
}
if (!IsValidImageContentType(image.ContentType))
{
throw new InvalidExtensionImageUploadException("The uploaded file is not a valid image.");
}

string developer = AuthenticationHelper.GetDeveloperUserName(HttpContext);

string imageName = GetFileNameFromUploadedFile(image);
try
{
var editingContext = AltinnRepoEditingContext.FromOrgRepoDeveloper(org, app, developer);
await _imagesService.UploadImage(editingContext, imageName, image.OpenReadStream(), overrideExisting);
return NoContent();
}
catch (InvalidOperationException e)
{
return BadRequest(e.Message);
}
}

/// <summary>
/// Endpoint for deleting image from application.
/// </summary>
/// <param name="org">Unique identifier of the organisation responsible for the app.</param>
/// <param name="app">Application identifier which is unique within an organisation.</param>
/// <param name="encodedImagePath">Relative encoded path of image to delete</param>
[HttpDelete("{encodedImagePath}")]
[ProducesResponseType(StatusCodes.Status200OK)]
public async Task<ActionResult> DeleteImage(string org, string app, [FromRoute] string encodedImagePath)
{
string developer = AuthenticationHelper.GetDeveloperUserName(HttpContext);
string decodedImagePath = HttpUtility.UrlDecode(encodedImagePath);
var editingContext = AltinnRepoEditingContext.FromOrgRepoDeveloper(org, app, developer);

await _imagesService.DeleteImage(editingContext, decodedImagePath);

return NoContent();
}

private static string GetFileNameFromUploadedFile(IFormFile image)
{
return ContentDispositionHeaderValue.Parse(new StringSegment(image.ContentDisposition)).FileName.ToString();
}

private bool IsValidImageContentType(string contentType)
{
return contentType.ToLower().StartsWith("image/");
}

}
2 changes: 1 addition & 1 deletion backend/src/Designer/Controllers/PreviewController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -132,7 +132,7 @@ public FileStreamResult Image(string org, string app, string imageFilePath, Canc

string developer = AuthenticationHelper.GetDeveloperUserName(_httpContextAccessor.HttpContext);
AltinnAppGitRepository altinnAppGitRepository = _altinnGitRepositoryFactory.GetAltinnAppGitRepository(org, app, developer);
Stream imageStream = altinnAppGitRepository.GetImage(imageFilePath);
Stream imageStream = altinnAppGitRepository.GetImageAsStreamByFilePath(imageFilePath);
return new FileStreamResult(imageStream, MimeTypeMap.GetMimeType(Path.GetExtension(imageFilePath).ToLower()));
}

Expand Down
47 changes: 27 additions & 20 deletions backend/src/Designer/Controllers/RepositoryController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,43 +9,39 @@
using Altinn.Studio.Designer.Configuration;
using Altinn.Studio.Designer.Enums;
using Altinn.Studio.Designer.Helpers;
using Altinn.Studio.Designer.Hubs.SyncHub;
using Altinn.Studio.Designer.Models;
using Altinn.Studio.Designer.RepositoryClient.Model;
using Altinn.Studio.Designer.Services.Interfaces;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.SignalR;
using RepositoryModel = Altinn.Studio.Designer.RepositoryClient.Model.Repository;

namespace Altinn.Studio.Designer.Controllers
{
/// <summary>
/// This is the API controller for functionality related to repositories.
/// </summary>
/// <remarks>
/// Initializes a new instance of the <see cref="RepositoryController"/> class.
/// </remarks>
/// <param name="giteaWrapper">the gitea wrapper</param>
/// <param name="sourceControl">the source control</param>
/// <param name="repository">the repository control</param>
/// <param name="userRequestsSynchronizationService">An <see cref="IUserRequestsSynchronizationService"/> used to control parallel execution of user requests.</param>
/// <param name="syncHub">websocket syncHub</param>
[Authorize]
[AutoValidateAntiforgeryToken]
[Route("designer/api/repos")]
public class RepositoryController : ControllerBase
public class RepositoryController(IGitea giteaWrapper, ISourceControl sourceControl, IRepository repository, IUserRequestsSynchronizationService userRequestsSynchronizationService, IHubContext<SyncHub, ISyncClient> syncHub) : ControllerBase
{
private readonly IGitea _giteaApi;
private readonly ISourceControl _sourceControl;
private readonly IRepository _repository;
private readonly IUserRequestsSynchronizationService _userRequestsSynchronizationService;

/// <summary>
/// Initializes a new instance of the <see cref="RepositoryController"/> class.
/// </summary>
/// <param name="giteaWrapper">the gitea wrapper</param>
/// <param name="sourceControl">the source control</param>
/// <param name="repository">the repository control</param>
/// <param name="userRequestsSynchronizationService">An <see cref="IUserRequestsSynchronizationService"/> used to control parallel execution of user requests.</param>
public RepositoryController(IGitea giteaWrapper, ISourceControl sourceControl, IRepository repository, IUserRequestsSynchronizationService userRequestsSynchronizationService)
{
_giteaApi = giteaWrapper;
_sourceControl = sourceControl;
_repository = repository;
_userRequestsSynchronizationService = userRequestsSynchronizationService;
}
private readonly IGitea _giteaApi = giteaWrapper;
private readonly ISourceControl _sourceControl = sourceControl;
private readonly IRepository _repository = repository;
private readonly IUserRequestsSynchronizationService _userRequestsSynchronizationService = userRequestsSynchronizationService;
private readonly IHubContext<SyncHub, ISyncClient> _syncHub = syncHub;

/// <summary>
/// Returns a list over repositories
Expand Down Expand Up @@ -323,6 +319,17 @@ public async Task CommitAndPushRepo([FromBody] CommitInfo commitInfo)
{
await _sourceControl.PushChangesForRepository(commitInfo);
}
catch (LibGit2Sharp.NonFastForwardException)
{
RepoStatus repoStatus = await _sourceControl.PullRemoteChanges(commitInfo.Org, commitInfo.Repository);
await _sourceControl.Push(commitInfo.Org, commitInfo.Repository);
foreach (RepositoryContent repoContent in repoStatus?.ContentStatus)
{
Source source = new(Path.GetFileName(repoContent.FilePath), repoContent.FilePath);
SyncSuccess syncSuccess = new(source);
await _syncHub.Clients.Group(developer).FileSyncSuccess(syncSuccess);
}
}
finally
{
semaphore.Release();
Expand Down
18 changes: 18 additions & 0 deletions backend/src/Designer/Enums/ImageUrlValidationResult.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
using System.Runtime.Serialization;

namespace Altinn.Studio.Designer.Enums;

/// <summary>
/// ImageUrlValidationResult
/// </summary>
public enum ImageUrlValidationResult
{
[EnumMember(Value = "Ok")]
Ok,

[EnumMember(Value = "NotAnImage")]
NotAnImage,

[EnumMember(Value = "NotValidUrl")]
NotValidUrl
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
using System;

namespace Altinn.Studio.Designer.Exceptions.AppDevelopment;

/// <summary>
/// Indicates that a file was uploaded with the a conflicting file name
/// </summary>
[Serializable]
public class ConflictingFileNameException : Exception
{
/// <inheritdoc/>
public ConflictingFileNameException()
{
}

/// <inheritdoc/>
public ConflictingFileNameException(string message) : base(message)
{
}

/// <inheritdoc/>
public ConflictingFileNameException(string message, Exception innerException) : base(message, innerException)
{
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
using System;

namespace Altinn.Studio.Designer.Exceptions.AppDevelopment
{
/// <summary>
/// Indicates that an image with invalid extension was uploaded
/// </summary>
[Serializable]
public class InvalidExtensionImageUploadException : Exception
{
/// <inheritdoc/>
public InvalidExtensionImageUploadException()
{
}

/// <inheritdoc/>
public InvalidExtensionImageUploadException(string message) : base(message)
{
}

/// <inheritdoc/>
public InvalidExtensionImageUploadException(string message, Exception innerException) : base(message, innerException)
{
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
namespace Altinn.Studio.Designer.Exceptions.AppDevelopment
{
/// <summary>
/// Indicates that an error occurred during C# code generation.
/// Indicates that a layout set id is invalid
/// </summary>
[Serializable]
public class InvalidLayoutSetIdException : Exception
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ public class AppDevelopmentErrorCodes
public const string NonUniqueLayoutSetIdError = "AD_01";
public const string NonUniqueTaskForLayoutSetError = "AD_02";
public const string EmptyLayoutSetIdError = "AD_03";
public const string ConflictingFileNameError = "AD_04";
public const string UploadedImageNotValid = nameof(UploadedImageNotValid);
}
}

Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,14 @@ public override void OnException(ExceptionContext context)
{
context.Result = new ObjectResult(ProblemDetailsUtils.GenerateProblemDetails(context.Exception, AppDevelopmentErrorCodes.EmptyLayoutSetIdError, HttpStatusCode.BadRequest)) { StatusCode = (int)HttpStatusCode.BadRequest };
}
if (context.Exception is ConflictingFileNameException)
{
context.Result = new ObjectResult(ProblemDetailsUtils.GenerateProblemDetails(context.Exception, AppDevelopmentErrorCodes.ConflictingFileNameError, HttpStatusCode.BadRequest)) { StatusCode = (int)HttpStatusCode.BadRequest };
}
if (context.Exception is InvalidExtensionImageUploadException)
{
context.Result = new ObjectResult(ProblemDetailsUtils.GenerateProblemDetails(context.Exception, AppDevelopmentErrorCodes.UploadedImageNotValid, HttpStatusCode.BadRequest)) { StatusCode = (int)HttpStatusCode.BadRequest };
}
}
}
}
Loading

0 comments on commit 6a6a130

Please sign in to comment.