OrchardCMS / OrchardCore

Orchard Core is an open-source modular and multi-tenant application framework built with ASP.NET Core, and a content management system (CMS) built on top of that framework.
https://orchardcore.net
BSD 3-Clause "New" or "Revised" License
7.35k stars 2.37k forks source link

Does Form support file upload? #6027

Open dodyg opened 4 years ago

dodyg commented 4 years ago

I didn't see anything in the Workflow nor the Form widget support for "multipart/form-data"

dodyg commented 4 years ago

Or more importantly does the workflow support file upload?

deanmarcussen commented 4 years ago

There is no support for file upload through workflows right now.

Related issues https://github.com/OrchardCMS/OrchardCore/issues/4896

dodyg commented 4 years ago

OK - let me try to implement a task for the workflow that handle a file upload.

dodyg commented 4 years ago

I am trying to write a simple Activity first just to get a hang on this custom activity creation thing. This one will write a message to the disk when it is done.

disk-writer

This thumbnail is handled by DiskWriterTask.Fields.Thumbnail.cshtml.

dodyg commented 4 years ago

This is a good simple activity task to learn from https://github.com/EtchUK/Etch.OrchardCore.Workflows/tree/master/FormOutput

dodyg commented 4 years ago

If anyone need similar functionality, this code will work

  public class DiskWriterTask : TaskActivity
    {
        readonly IOptions<ShellOptions> _shellOptions;
        readonly IStringLocalizer S;
        readonly ShellSettings _shellSettings;
        private readonly IHttpContextAccessor _http;

        public DiskWriterTask(IStringLocalizer<DiskWriterTask> s, 
            IOptions<ShellOptions> shellOptions, 
            ShellSettings shellSettings,
            IHttpContextAccessor httpContextAccessor)
        {
            _shellOptions = shellOptions;
            _shellSettings = shellSettings;
            S = s;
            _http = httpContextAccessor;
        }

        public override string Name => nameof(DiskWriterTask);

        public override LocalizedString DisplayText => S["Disk Writer Task"];

        public override LocalizedString Category => S["UI"];

        public override IEnumerable<Outcome> GetPossibleOutcomes(WorkflowExecutionContext workflowContext, ActivityContext activityContext)
        {
            return Outcomes(S["Done"]);
        }

        public override bool CanExecute(WorkflowExecutionContext workflowContext, ActivityContext activityContext)
        {
            return _http.HttpContext?.Request?.Form != null;
        }

        public override async Task<ActivityExecutionResult> ExecuteAsync(WorkflowExecutionContext workflowContext, ActivityContext activityContext)
        {
            var shell = _shellOptions.Value;
            var directory = PathExtensions.Combine(shell.ShellsApplicationDataPath, shell.ShellsContainerName, _shellSettings.Name, Folder);

            if (!Directory.Exists(directory))
                Directory.CreateDirectory(directory);

            foreach(var file in _http.HttpContext.Request.Form.Files)
            {
                var toSave = PathExtensions.Combine(directory, file.FileName);
                using (var stream = System.IO.File.Create(toSave))
                {
                    await file.CopyToAsync(stream); 
                    workflowContext.Properties[file.Name] = toSave;
                }
            }

            return Outcomes("Done");
        }

        public string Folder
        {
            get => GetProperty<string>();
            set => SetProperty(value ?? string.Empty);
        }
    }

    public class DiskWriterViewModel
    {
        public string Folder { get; set; }
    }

    public class DiskWriterTaskDisplay: ActivityDisplayDriver<DiskWriterTask, DiskWriterViewModel>
    {
        protected override void EditActivity(DiskWriterTask activity, DiskWriterViewModel model)
        {
            model.Folder = activity.Folder;
        }

        protected override void UpdateActivity(DiskWriterViewModel model, DiskWriterTask activity)
        {
            activity.Folder = model.Folder;
        }
    }
dodyg commented 4 years ago

You will have to fill the rest of the views.

layout

dodyg commented 4 years ago

test

dodyg commented 4 years ago

This is just a ContentItem with FlowPart form-2

This is how it renders form-1

dodyg commented 4 years ago

Because the Form widget doesn't support multiple, you have to modify the generated html with Javascript

$(function(){
    $('form').attr("enctype","multipart/form-data");
});
dodyg commented 4 years ago

This is the property page of the DiskWriter activity. The one I added is just Folder. The Tittle input comes by default.

disk-writer-edit

dodyg commented 4 years ago

This is my first time writing a custom activity task so this one is gonna be rough at the edges.

dodyg commented 4 years ago

Simple worklflow that will add data from the form

property-task-3


Pay attention to this one. There are older samples that uses JSON.parse(readBody()) but that stopped working a while ago. The name of the property here will be accessible as Workflow.Properties.YourPropertyName. So in this case, this property can be access as Workflow.Properties.ApplicationDetails in Liquid.

property-task


This part is the one that insert the data from the form to your content. property-task-2

amira-elbatal commented 4 years ago

@dodyg can upload your solution ?

dodyg commented 4 years ago

There you go. Disk Writer.zip

You need to modify the code to make it more robust but all the necessary ingredients are there.

amira-elbatal commented 4 years ago

@dodyg thanks.

hishamco commented 4 years ago

Thanks a lot @dodyg .. very detailed and useful explanation as usual 🥇

duncanhoggan commented 4 years ago

@dodyg I updated your Task to include liquid support on the folder.

DiskWriterTask.cs

Flarescape commented 4 years ago

I've done something similar around two months ago and maybe I'm allowed to add some thoughts about security and validation. In my case, I've added some more fields to the file upload activity and some more output options:

Fields:

Optional fields:

Feature field:

stevetayloruk commented 4 years ago

I think the save part needs to change so if someone is using cloud blob storage then it would also work. Seems like its hardcoded to the local filesystem.

dodyg commented 4 years ago

@stevetayloruk Yup. This is just a quick and dirty implementation for my local project purpose.

sebastienros commented 4 years ago

What about having such a task by default in the project? I assume we would need to use a custom abstraction of the FS, such that we could write to a custom folder or store (blob). These files should not be servable by default. Or have an option to store them in a specific media folder, if we want them to be servable.

Maybe it should only use the media service. And we'd need to have a way to use a custom folder that can't serve files publicly. But we'd be able to browse them from the admin.

Flarescape commented 4 years ago

@sebastienros This would be really great. I mean, I'm currently working with my own solution as described, but it's far from perfect and not for everyones needs. In addition to that, there is a very common scenario, where people are attaching files to a form: a job application. With that, you have two Options to handle the files: 1.) Store them inside the media/file system 2.) Send them by mail with a workflow

In case of option 2, I've extended the OrchardCore.Email module to support attachments which would be really helpfull as default functionality. My file upload task sets the paths as workflow variables which the extended email module reads and attaches them to the mail. This could be solved way better, but it works for now.

The ability to choose wheter a media folder is public or private would be awesome, because I would really like to store files inside the media, that are only available through the admin panel, so I don't have to save my files outside of the media folder with a hardcoded path anymore.

stevetayloruk commented 3 years ago

@sebastienros Is there any plans to have media folders/files securable through the permission system?

What would your approach be to achieve this?

sebastienros commented 3 years ago

Per role, per folder:

In the attached media field, we already do that, by forcing a custom folder, and not showing other ones.

Bonus feature (harder):

I think it should be done at the folder level to simplify the UI.

Piedone commented 6 months ago

The Form widget now supports multipart/form-data. If https://github.com/OrchardCMS/OrchardCore/pull/12218 is merged, we'll have file uploads in Workflows too.

The most recent conversation here changed over to authorized Media (file storage) access though. For that, we have this other issue: https://github.com/OrchardCMS/OrchardCore/issues/3590.