mirror of
https://github.com/OrchardCMS/Orchard.git
synced 2025-10-15 19:54:57 +08:00
Implementing Signal activities and services for Orchard.Workflows
--HG-- branch : 1.x
This commit is contained in:
@@ -4,15 +4,20 @@ using Orchard.Workflows.Models;
|
||||
using Orchard.Workflows.Services;
|
||||
|
||||
namespace Orchard.Workflows.Activities {
|
||||
public class WebResponseActivity : Event {
|
||||
public WebResponseActivity() {
|
||||
/// <summary>
|
||||
/// Represents a named event which can be triggered by any kind of activity.
|
||||
/// </summary>
|
||||
public class SignalActivity : Event {
|
||||
public const string SignalEventName = "Signal";
|
||||
|
||||
public SignalActivity() {
|
||||
T = NullLocalizer.Instance;
|
||||
}
|
||||
|
||||
public Localizer T { get; set; }
|
||||
|
||||
public override bool CanExecute(WorkflowContext workflowContext, ActivityContext activityContext) {
|
||||
return true;
|
||||
return activityContext.GetState<string>(SignalEventName) == workflowContext.Tokens[SignalEventName].ToString();
|
||||
}
|
||||
|
||||
public override bool CanStartWorkflow {
|
||||
@@ -28,15 +33,21 @@ namespace Orchard.Workflows.Activities {
|
||||
}
|
||||
|
||||
public override string Name {
|
||||
get { return "WebResponse"; }
|
||||
get { return SignalEventName; }
|
||||
}
|
||||
|
||||
public override LocalizedString Category {
|
||||
get { return T("HTTP"); }
|
||||
get { return T("Events"); }
|
||||
}
|
||||
|
||||
public override LocalizedString Description {
|
||||
get { return T("Suspends the workflow until an HTTP request comes in."); }
|
||||
get { return T("Suspends the workflow until this signal is specifically triggered."); }
|
||||
}
|
||||
|
||||
public override string Form {
|
||||
get {
|
||||
return "SignalEvent";
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@@ -120,8 +120,8 @@ namespace Orchard.Workflows.Activities {
|
||||
|
||||
|
||||
foreach (var action in awaiting) {
|
||||
var tokens = new Dictionary<string, object> { { "Content", _contentManager.Get(action.WorkflowRecord.ContentItemRecord.Id, VersionOptions.Latest) } };
|
||||
var contentItem = _contentManager.Get(action.WorkflowRecord.ContentItemRecord.Id, VersionOptions.Latest);
|
||||
var tokens = new Dictionary<string, object> { { "Content", contentItem } };
|
||||
var workflowState = FormParametersHelper.FromJsonString(action.WorkflowRecord.State);
|
||||
workflowState.TimerActivity_StartedUtc = null;
|
||||
action.WorkflowRecord.State = FormParametersHelper.ToJsonString(workflowState);
|
||||
|
@@ -0,0 +1,50 @@
|
||||
using System.Collections.Generic;
|
||||
using Orchard.Localization;
|
||||
using Orchard.Workflows.Models;
|
||||
using Orchard.Workflows.Services;
|
||||
|
||||
namespace Orchard.Workflows.Activities {
|
||||
public class TriggerActivity : Task {
|
||||
private readonly IWorkflowManager _workflowManager;
|
||||
|
||||
public TriggerActivity(IWorkflowManager workflowManager) {
|
||||
_workflowManager = workflowManager;
|
||||
T = NullLocalizer.Instance;
|
||||
}
|
||||
|
||||
public Localizer T { get; set; }
|
||||
|
||||
public override bool CanExecute(WorkflowContext workflowContext, ActivityContext activityContext) {
|
||||
return true;
|
||||
}
|
||||
|
||||
public override IEnumerable<LocalizedString> GetPossibleOutcomes(WorkflowContext workflowContext, ActivityContext activityContext) {
|
||||
yield return T("Done");
|
||||
}
|
||||
|
||||
public override IEnumerable<LocalizedString> Execute(WorkflowContext workflowContext, ActivityContext activityContext) {
|
||||
var tokens = new Dictionary<string, object> { { "Content", workflowContext.Content }, { SignalActivity.SignalEventName, activityContext.GetState<string>(SignalActivity.SignalEventName) } };
|
||||
_workflowManager.TriggerEvent(SignalActivity.SignalEventName, workflowContext.Content, () => tokens);
|
||||
|
||||
yield return T("Done");
|
||||
}
|
||||
|
||||
public override string Name {
|
||||
get { return "Trigger"; }
|
||||
}
|
||||
|
||||
public override LocalizedString Category {
|
||||
get { return T("Events"); }
|
||||
}
|
||||
|
||||
public override LocalizedString Description {
|
||||
get { return T("Triggers a Signal by its name."); }
|
||||
}
|
||||
|
||||
public override string Form {
|
||||
get {
|
||||
return "Trigger";
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@@ -85,7 +85,7 @@ namespace Orchard.Workflows.Activities {
|
||||
}
|
||||
|
||||
public override LocalizedString Description {
|
||||
get { return T("Performs an HTTP GET or POST request on the specified URL and stores the response as part of the workflow instance."); }
|
||||
get { return T("Performs an HTTP request."); }
|
||||
}
|
||||
|
||||
public override string Form {
|
||||
|
@@ -0,0 +1,61 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Web.Mvc;
|
||||
using Orchard.ContentManagement;
|
||||
using Orchard.Logging;
|
||||
using Orchard.Workflows.Activities;
|
||||
using Orchard.Workflows.Services;
|
||||
|
||||
namespace Orchard.Workflows.Controllers {
|
||||
public class SignalController : Controller {
|
||||
private readonly IWorkflowManager _workflowManager;
|
||||
private readonly ISignalService _genericEventService;
|
||||
private readonly IContentManager _contentManager;
|
||||
|
||||
public SignalController(
|
||||
IWorkflowManager workflowManager,
|
||||
ISignalService genericEventService,
|
||||
IContentManager contentManager) {
|
||||
_workflowManager = workflowManager;
|
||||
_genericEventService = genericEventService;
|
||||
_contentManager = contentManager;
|
||||
}
|
||||
|
||||
public ILogger Logger { get; set; }
|
||||
|
||||
// This could be invoked by external applications and services to trigger an event within the workflows.
|
||||
public ActionResult Trigger(string nonce) {
|
||||
|
||||
int contentItemId;
|
||||
string signal;
|
||||
|
||||
if (!_genericEventService.DecryptNonce(nonce, out contentItemId, out signal)) {
|
||||
Logger.Debug("Invalid nonce provided: " + nonce);
|
||||
return HttpNotFound();
|
||||
}
|
||||
|
||||
var contentItem = _contentManager.Get(contentItemId, VersionOptions.Latest);
|
||||
|
||||
if (contentItem == null) {
|
||||
Logger.Debug("Could not find specified content item in none: " + contentItemId);
|
||||
return HttpNotFound();
|
||||
}
|
||||
|
||||
// Right now, all workflow instances that are at the WebRequest activity node would continue as soon as a request
|
||||
// to this action comes in, but that should not happen; it should be controlled by activity configuration and evaluations.
|
||||
_workflowManager.TriggerEvent(SignalActivity.SignalEventName, contentItem, () => {
|
||||
var dictionary = new Dictionary<string, object> { { "Content", contentItem }, { SignalActivity.SignalEventName, signal } };
|
||||
|
||||
// Let's include query string stuff, so that the WebRequest activity can
|
||||
// potentially match against certain parameters to decide whether or not it should execute,
|
||||
// based on its configuration (yet to be defined).
|
||||
Request.QueryString.CopyTo(dictionary);
|
||||
Request.Form.CopyTo(dictionary);
|
||||
|
||||
return dictionary;
|
||||
});
|
||||
|
||||
// 200
|
||||
return new EmptyResult();
|
||||
}
|
||||
}
|
||||
}
|
@@ -1,36 +0,0 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Web.Mvc;
|
||||
using Orchard.Workflows.Services;
|
||||
|
||||
namespace Orchard.Workflows.Controllers {
|
||||
public class WorkflowController : Controller {
|
||||
private readonly IWorkflowManager _workflowManager;
|
||||
|
||||
public WorkflowController(IWorkflowManager workflowManager) {
|
||||
_workflowManager = workflowManager;
|
||||
}
|
||||
|
||||
// This could be invoked by external applications and services to trigger an event within the workflows.
|
||||
public ActionResult Callback() {
|
||||
|
||||
// Right now, all workflow instances that are at the WebRequest activity node would continue as soon as a request
|
||||
// to this action comes in, but that should not happen; it should be controlled by activity configuration and evaluations.
|
||||
_workflowManager.TriggerEvent("WebRequest", null, () => {
|
||||
var dictionary = new Dictionary<string, object>();
|
||||
|
||||
// Let's include query string stuff, so that the WebRequest activity can
|
||||
// potentially match against certain parameters to decide whether or not it should execute,
|
||||
// based on its configuration (yet to be defined).
|
||||
Request.QueryString.CopyTo(dictionary);
|
||||
|
||||
return dictionary;
|
||||
});
|
||||
|
||||
// A Redirect may have been set by one of the rule events.
|
||||
if (!string.IsNullOrEmpty(HttpContext.Response.RedirectLocation))
|
||||
return new EmptyResult();
|
||||
|
||||
return new EmptyResult(); // Or maybe an "OK" string. It shouldn't really matter, just as long as we return HTTP 200 OK.
|
||||
}
|
||||
}
|
||||
}
|
@@ -0,0 +1,80 @@
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Web.Mvc;
|
||||
using System.Xml.Linq;
|
||||
using Newtonsoft.Json;
|
||||
using Orchard.Data;
|
||||
using Orchard.DisplayManagement;
|
||||
using Orchard.Forms.Services;
|
||||
using Orchard.Localization;
|
||||
using Orchard.Workflows.Activities;
|
||||
using Orchard.Workflows.Models;
|
||||
|
||||
namespace Orchard.Workflows.Forms {
|
||||
public class SignalForms : IFormProvider {
|
||||
private readonly IRepository<ActivityRecord> _activityRecords;
|
||||
protected dynamic Shape { get; set; }
|
||||
public Localizer T { get; set; }
|
||||
|
||||
public SignalForms(IShapeFactory shapeFactory, IRepository<ActivityRecord> activityRecords) {
|
||||
_activityRecords = activityRecords;
|
||||
Shape = shapeFactory;
|
||||
T = NullLocalizer.Instance;
|
||||
}
|
||||
|
||||
public void Describe(DescribeContext context) {
|
||||
Func<IShapeFactory, dynamic> form =
|
||||
shape => {
|
||||
return Shape.Form(
|
||||
Id: "SignalEvent",
|
||||
_Name: Shape.Textbox(
|
||||
Id: "signal", Name: "Signal",
|
||||
Title: T("Name of the signal."),
|
||||
Description: T("The name of the signal."),
|
||||
Classes: new[] {"textMedium"})
|
||||
);
|
||||
};
|
||||
|
||||
context.Form("SignalEvent", form);
|
||||
|
||||
form =
|
||||
shape => {
|
||||
var f = Shape.Form(
|
||||
Id: "OneOfSignals",
|
||||
_Parts: Shape.SelectList(
|
||||
Id: "signal", Name: "Signal",
|
||||
Title: T("Available signals"),
|
||||
Description: T("Select a signal."),
|
||||
Size: 1,
|
||||
Multiple: false
|
||||
)
|
||||
);
|
||||
|
||||
var allEvents = _activityRecords
|
||||
.Table
|
||||
.Where(x => x.Name == SignalActivity.SignalEventName)
|
||||
.Select(x => GetState(x.State))
|
||||
.ToArray()
|
||||
.Select(x => (string)x.Signal);
|
||||
|
||||
foreach (var signal in allEvents) {
|
||||
f._Parts.Add(new SelectListItem { Value = signal, Text = signal });
|
||||
}
|
||||
|
||||
return f;
|
||||
};
|
||||
|
||||
context.Form("Trigger", form);
|
||||
}
|
||||
|
||||
private dynamic GetState(string state) {
|
||||
if (!String.IsNullOrWhiteSpace(state)) {
|
||||
var formatted = JsonConvert.DeserializeXNode(state, "Root").ToString();
|
||||
var serialized = String.IsNullOrEmpty(formatted) ? "{}" : JsonConvert.SerializeXNode(XElement.Parse(formatted));
|
||||
return FormParametersHelper.FromJsonString(serialized).Root;
|
||||
}
|
||||
|
||||
return FormParametersHelper.FromJsonString("{}");
|
||||
}
|
||||
}
|
||||
}
|
@@ -39,12 +39,14 @@ namespace Orchard.Workflows.Forms {
|
||||
_FormValues: New.Textarea(
|
||||
Id: "FormValues", Name: "FormValues",
|
||||
Title: T("Form Values"),
|
||||
Description: T("For KeyValue, enter one line per key=value pair to submit when using the POST verb. For JSON, enter a valid JSON string"),
|
||||
Description: T("For KeyValue, enter one line per key=value pair to submit when using the POST verb. For JSon, enter a valid JSon string"),
|
||||
Classes: new[] {"tokenized"})
|
||||
);
|
||||
|
||||
form._Verb.Add(new SelectListItem { Value = "GET", Text = "GET" });
|
||||
form._Verb.Add(new SelectListItem { Value = "POST", Text = "POST" });
|
||||
form._Verb.Add(new SelectListItem { Value = "PUT", Text = "PUT" });
|
||||
form._Verb.Add(new SelectListItem { Value = "DELETE", Text = "DELETE" });
|
||||
|
||||
form._FormFormat.Add(new SelectListItem { Value = "KeyValue", Text = "Key / Value" });
|
||||
form._FormFormat.Add(new SelectListItem { Value = "Json", Text = "Json" });
|
||||
|
@@ -130,6 +130,8 @@
|
||||
<Compile Include="Activities\BranchActivity.cs" />
|
||||
<Compile Include="Activities\AssignRoleActivity.cs" />
|
||||
<Compile Include="Activities\CloseCommentsActivity.cs" />
|
||||
<Compile Include="Activities\TriggerActivity.cs" />
|
||||
<Compile Include="Activities\SignalActivity.cs" />
|
||||
<Compile Include="Activities\MergeBranchActivity.cs" />
|
||||
<Compile Include="Activities\DeleteActivity.cs" />
|
||||
<Compile Include="Activities\ExclusiveBranchActivity.cs" />
|
||||
@@ -138,8 +140,8 @@
|
||||
<Compile Include="Activities\RedirectActivity.cs" />
|
||||
<Compile Include="Activities\TimerActivity.cs" />
|
||||
<Compile Include="Activities\WebRequestActivity.cs" />
|
||||
<Compile Include="Activities\WebResponseActivity.cs" />
|
||||
<Compile Include="Controllers\WorkflowController.cs" />
|
||||
<Compile Include="Controllers\SignalController.cs" />
|
||||
<Compile Include="Forms\SignalForms.cs" />
|
||||
<Compile Include="Forms\RedirectActionForm.cs" />
|
||||
<Compile Include="Forms\TimerForms.cs" />
|
||||
<Compile Include="Forms\BranchForms.cs" />
|
||||
@@ -169,6 +171,8 @@
|
||||
<Compile Include="Forms\UserTaskForms.cs" />
|
||||
<Compile Include="Forms\NotificationActivityForms.cs" />
|
||||
<Compile Include="ResourceManifest.cs" />
|
||||
<Compile Include="Services\GenericEventService.cs" />
|
||||
<Compile Include="Services\ISignalService.cs" />
|
||||
<Compile Include="Services\Task.cs" />
|
||||
<Compile Include="Services\Event.cs" />
|
||||
<Compile Include="Services\IActivity.cs" />
|
||||
@@ -176,6 +180,7 @@
|
||||
<Compile Include="Services\IActivitiesManager.cs" />
|
||||
<Compile Include="Services\IWorkflowManager.cs" />
|
||||
<Compile Include="Services\WorkflowManager.cs" />
|
||||
<Compile Include="Tokens\SignalTokens.cs" />
|
||||
<Compile Include="ViewModels\AdminEditViewModel.cs" />
|
||||
<Compile Include="ViewModels\AdminIndexViewModel.cs" />
|
||||
<Compile Include="ViewModels\WorkflowDefinitionViewModel.cs" />
|
||||
|
@@ -150,10 +150,11 @@ var bindForm = function(form, data) {
|
||||
}
|
||||
break;
|
||||
case 'select':
|
||||
$el.find('option').each(function () {
|
||||
var self = $(this);
|
||||
self.attr('selected', values.indexOf(self.attr('value')) != -1);
|
||||
});
|
||||
$el.val(values);
|
||||
//$el.find('option').each(function () {
|
||||
// var self = $(this);
|
||||
// self.attr('selected', values.indexOf(self.attr('value')) != -1);
|
||||
//});
|
||||
break;
|
||||
default:
|
||||
$el.val(val);
|
||||
|
@@ -0,0 +1,38 @@
|
||||
using System;
|
||||
using System.Text;
|
||||
using System.Xml.Linq;
|
||||
using Orchard.Security;
|
||||
|
||||
namespace Orchard.Workflows.Services {
|
||||
public class SignalService : ISignalService {
|
||||
private readonly IEncryptionService _encryptionService;
|
||||
|
||||
public SignalService(IEncryptionService encryptionService) {
|
||||
_encryptionService = encryptionService;
|
||||
}
|
||||
|
||||
public string CreateNonce(int contentItemId, string signal) {
|
||||
var challengeToken = new XElement("n", new XAttribute("c", contentItemId), new XAttribute("n", signal)).ToString();
|
||||
var data = Encoding.UTF8.GetBytes(challengeToken);
|
||||
return Convert.ToBase64String(_encryptionService.Encode(data));
|
||||
}
|
||||
|
||||
public bool DecryptNonce(string nonce, out int contentItemId, out string signal) {
|
||||
contentItemId = 0;
|
||||
signal = "";
|
||||
|
||||
try {
|
||||
var data = _encryptionService.Decode(Convert.FromBase64String(nonce));
|
||||
var xml = Encoding.UTF8.GetString(data);
|
||||
var element = XElement.Parse(xml);
|
||||
contentItemId = Convert.ToInt32(element.Attribute("c").Value);
|
||||
signal = element.Attribute("n").Value;
|
||||
return true;
|
||||
}
|
||||
catch {
|
||||
return false;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
@@ -0,0 +1,6 @@
|
||||
namespace Orchard.Workflows.Services {
|
||||
public interface ISignalService : IDependency {
|
||||
string CreateNonce(int contentItemId, string signal);
|
||||
bool DecryptNonce(string nonce, out int contentItemId, out string signal);
|
||||
}
|
||||
}
|
@@ -85,6 +85,8 @@ namespace Orchard.Workflows.Services {
|
||||
Record = awaitingActivityRecord.WorkflowRecord
|
||||
};
|
||||
|
||||
workflowContext.Tokens["Workflow"] = workflowContext;
|
||||
|
||||
var activityContext = CreateActivityContext(awaitingActivityRecord.ActivityRecord, tokens);
|
||||
|
||||
// check the condition
|
||||
@@ -109,6 +111,8 @@ namespace Orchard.Workflows.Services {
|
||||
Tokens = tokens,
|
||||
};
|
||||
|
||||
workflowContext.Tokens["Workflow"] = workflowContext;
|
||||
|
||||
var workflowRecord = new WorkflowRecord {
|
||||
WorkflowDefinitionRecord = activityRecord.WorkflowDefinitionRecord,
|
||||
State = "{}",
|
||||
|
@@ -0,0 +1,48 @@
|
||||
using System;
|
||||
using System.Web;
|
||||
using System.Web.Mvc;
|
||||
using Orchard.Localization;
|
||||
using Orchard.Mvc.Extensions;
|
||||
using Orchard.Tokens;
|
||||
using Orchard.Workflows.Models;
|
||||
using Orchard.Workflows.Services;
|
||||
|
||||
namespace Orchard.Workflows.Tokens {
|
||||
public class SignalTokens : ITokenProvider {
|
||||
private readonly IWorkContextAccessor _workContextAccessor;
|
||||
private readonly Lazy<ISignalService> _signalService;
|
||||
|
||||
public SignalTokens(IWorkContextAccessor workContextAccessor, Lazy<ISignalService> signalService) {
|
||||
_workContextAccessor = workContextAccessor;
|
||||
_signalService = signalService;
|
||||
T = NullLocalizer.Instance;
|
||||
}
|
||||
|
||||
public Localizer T { get; set; }
|
||||
|
||||
public void Describe(DescribeContext context) {
|
||||
context.For("Workflow", T("Workflow"), T("Workflow tokens."))
|
||||
.Token("TriggerUrl:*", T("TriggerUrl:<signal>"), T("The relative url to call in order to trigger the specified Signal."))
|
||||
;
|
||||
}
|
||||
|
||||
public void Evaluate(EvaluateContext context) {
|
||||
if (_workContextAccessor.GetContext().HttpContext == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
context.For<WorkflowContext>("Workflow")
|
||||
.Token(
|
||||
token => token.StartsWith("TriggerUrl:", StringComparison.OrdinalIgnoreCase) ? token.Substring("TriggerUrl:".Length) : null,
|
||||
(token, workflowContext) => {
|
||||
int contentItemId = 0;
|
||||
if (workflowContext.Content != null) {
|
||||
contentItemId = workflowContext.Content.Id;
|
||||
}
|
||||
|
||||
var url = "~/Orchard.Workflows/Signal/Trigger?nonce=" + HttpUtility.UrlEncode(_signalService.Value.CreateNonce(contentItemId, token));
|
||||
return new UrlHelper(_workContextAccessor.GetContext().HttpContext.Request.RequestContext).MakeAbsolute(url);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user