Skip to content

Commit cc37d85

Browse files
authored
Merge pull request #9 from umbraco/feature/zapier-integration
Feature/zapier integration
2 parents fc0c531 + 5cb3c1f commit cc37d85

File tree

11 files changed

+368
-2
lines changed

11 files changed

+368
-2
lines changed

.gitignore

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
## Ignore Visual Studio temporary files, build results, and
1+
## Ignore Visual Studio temporary files, build results, and
22
## files generated by popular Visual Studio add-ons.
33
##
44
## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore
@@ -397,4 +397,6 @@ src/Umbraco.Forms.Integrations.TestSite/scripts
397397
src/Umbraco.Forms.Integrations.TestSite/Umbraco
398398
src/Umbraco.Forms.Integrations.TestSite/Views
399399
*.zip
400-
appsettings.Local.json
400+
appsettings.Local.json
401+
src/Umbraco.Forms.Integrations.Automation.Zapier/app.config
402+
src/Umbraco.Forms.Integrations.Automation.Zapier/packages.config
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
using System.Linq;
2+
using System.Web.Http;
3+
4+
using Umbraco.Core.Services;
5+
using Umbraco.Forms.Integrations.Automation.Zapier.Models;
6+
using Umbraco.Web.WebApi;
7+
8+
namespace Umbraco.Forms.Integrations.Automation.Zapier.Controllers
9+
{
10+
public class AuthController : UmbracoApiController
11+
{
12+
[HttpPost]
13+
public bool ValidateUser([FromBody] UserModel userModel)
14+
{
15+
var isUserValid = Security.ValidateBackOfficeCredentials(userModel.Username, userModel.Password);
16+
if (!isUserValid) return false;
17+
18+
IUserService userService = Services.UserService;
19+
20+
var user = userService.GetByUsername(userModel.Username);
21+
22+
return user != null && user.Groups.Any(p => p.Name == userModel.UserGroup);
23+
}
24+
}
25+
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+

2+
namespace Umbraco.Forms.Integrations.Automation.Zapier.Models
3+
{
4+
public class UserModel
5+
{
6+
public string Username { get; set; }
7+
8+
public string Password { get; set; }
9+
10+
public string UserGroup { get; set; }
11+
}
12+
}
Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using System.Linq;
4+
5+
using Newtonsoft.Json;
6+
7+
using Umbraco.Core.Models.PublishedContent;
8+
using Umbraco.Forms.Core;
9+
using Umbraco.Forms.Core.Providers.Models;
10+
using Umbraco.Web;
11+
12+
namespace Umbraco.Forms.Integrations.Automation.Zapier.Services
13+
{
14+
/// <summary>
15+
/// Fluent Builder for packing fields mappings.
16+
/// </summary>
17+
public class FieldMappingBuilder: IFieldMappingBuilder
18+
{
19+
private readonly Dictionary<string, string> _content;
20+
21+
private readonly IUmbracoContextAccessor _umbracoContextAccessor;
22+
23+
public FieldMappingBuilder(IUmbracoContextAccessor umbracoContextAccessor)
24+
{
25+
_umbracoContextAccessor = umbracoContextAccessor;
26+
27+
_content = new Dictionary<string, string>();
28+
}
29+
30+
/// <summary>
31+
/// Add form fields to the request content sent to Zapier. If no propery is mapped, all fields will be included by their alias.
32+
/// </summary>
33+
/// <param name="mappings">Serialized details of the form fields</param>
34+
/// <param name="e">Form record details</param>
35+
/// <returns></returns>
36+
public IFieldMappingBuilder IncludeFieldsMappings(string mappings, RecordEventArgs e)
37+
{
38+
var mappingsList = JsonConvert.DeserializeObject<List<FieldMapping>>(mappings);
39+
if (mappingsList.Any())
40+
{
41+
foreach (var mapping in mappingsList)
42+
{
43+
var fieldRecord = !string.IsNullOrEmpty(mapping.Value) ? e.Record.RecordFields[Guid.Parse(mapping.Value)] : null;
44+
45+
_content.Add(mapping.Alias,
46+
fieldRecord != null
47+
? string.IsNullOrEmpty(mapping.StaticValue)
48+
? fieldRecord.ValuesAsString() : mapping.StaticValue
49+
: mapping.StaticValue);
50+
}
51+
}
52+
else
53+
{
54+
foreach (var recordField in e.Record.RecordFields)
55+
{
56+
_content.Add(recordField.Value.Alias, recordField.Value.ValuesAsString());
57+
}
58+
}
59+
60+
return this;
61+
}
62+
63+
/// <summary>
64+
/// Add form standard fields - that are set as Included in the settings of the workflow - to the request content sent to Zapier.
65+
/// </summary>
66+
/// <param name="mappings">Serialized details of the form standard fields</param>
67+
/// <param name="e">Form record details</param>
68+
/// <returns></returns>
69+
public IFieldMappingBuilder IncludeStandardFieldsMappings(string mappings, RecordEventArgs e)
70+
{
71+
if (!string.IsNullOrEmpty(mappings))
72+
{
73+
var standardFieldMappings =
74+
JsonConvert.DeserializeObject<IEnumerable<StandardFieldMapping>>(mappings).ToList();
75+
76+
foreach (StandardFieldMapping fieldMapping in standardFieldMappings.Where(x => x.Include))
77+
{
78+
switch (fieldMapping.Field)
79+
{
80+
case StandardField.FormId:
81+
_content.Add(fieldMapping.KeyName, e.Form.Id.ToString());
82+
break;
83+
case StandardField.FormName:
84+
_content.Add(fieldMapping.KeyName, e.Form.Name);
85+
break;
86+
case StandardField.PageUrl:
87+
UmbracoContext umbracoContext = _umbracoContextAccessor.UmbracoContext;
88+
var pageUrl = umbracoContext.UrlProvider.GetUrl(e.Record.UmbracoPageId, UrlMode.Absolute);
89+
_content.Add(fieldMapping.KeyName, pageUrl);
90+
break;
91+
case StandardField.SubmissionDate:
92+
_content.Add(fieldMapping.KeyName, e.Record.Created.ToString());
93+
break;
94+
default:
95+
throw new InvalidOperationException(
96+
$"The field '{fieldMapping.Field}' is not supported for including in the collection.");
97+
}
98+
}
99+
}
100+
101+
return this;
102+
}
103+
104+
public Dictionary<string, string> Map() => _content;
105+
}
106+
}
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
using System.Collections.Generic;
2+
3+
using Umbraco.Forms.Core;
4+
5+
namespace Umbraco.Forms.Integrations.Automation.Zapier.Services
6+
{
7+
public interface IFieldMappingBuilder
8+
{
9+
IFieldMappingBuilder IncludeFieldsMappings(string mappings, RecordEventArgs e);
10+
11+
IFieldMappingBuilder IncludeStandardFieldsMappings(string mappings, RecordEventArgs e);
12+
13+
Dictionary<string, string> Map();
14+
}
15+
}
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
<Project Sdk="Microsoft.NET.Sdk">
2+
3+
<PropertyGroup>
4+
<TargetFramework>net472</TargetFramework>
5+
</PropertyGroup>
6+
7+
<PropertyGroup>
8+
<PackageId>Umbraco.Forms.Integrations.Automation.Zapier</PackageId>
9+
<Title>Umbraco Forms Integrations: Automation - Zapier</Title>
10+
<Description>An extension for Umbraco Forms to add support for triggering zaps.</Description>
11+
<PackageIconUrl></PackageIconUrl>
12+
<PackageProjectUrl>https://github.com/umbraco/Umbraco.Forms.Integrations</PackageProjectUrl>
13+
<RepositoryUrl>https://github.com/umbraco/Umbraco.Forms.Integrations</RepositoryUrl>
14+
<Version>1.0.0</Version>
15+
<Authors>Umbraco HQ</Authors>
16+
<Company>Umbraco</Company>
17+
</PropertyGroup>
18+
19+
<ItemGroup>
20+
<PackageReference Include="UmbracoCms.Web" Version="8.1.0" />
21+
<PackageReference Include="UmbracoCms.Core" Version="8.1.0" />
22+
<PackageReference Include="UmbracoForms.Core" Version="8.9.1" />
23+
</ItemGroup>
24+
25+
<ItemGroup>
26+
<Folder Include="App_Plugins\UmbracoForms.Integrations\Automation\Zapier\" />
27+
</ItemGroup>
28+
29+
</Project>
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using System.Text.RegularExpressions;
4+
5+
namespace Umbraco.Forms.Integrations.Automation.Zapier.Validators
6+
{
7+
public class WebHookValidator : WorkflowBaseValidator<string>
8+
{
9+
private const string Pattern =
10+
"((http|https)://)(www.)?[a-zA-Z0-9@:%._\\+~#?&//=]{2,256}\\.[a-z]{2,6}\\b([-a-zA-Z0-9@:%._\\+~#?&//=]*)";
11+
12+
public override bool IsValid(string input, ref List<Exception> exceptions)
13+
{
14+
if (string.IsNullOrEmpty(input))
15+
{
16+
exceptions.Add(new Exception("URL is required"));
17+
return false;
18+
}
19+
20+
var regex = new Regex(Pattern);
21+
22+
var result = regex.IsMatch(input);
23+
24+
if(!result)
25+
exceptions.Add(new Exception("Invalid URL format"));
26+
27+
return result;
28+
}
29+
}
30+
}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
using System;
2+
using System.Collections.Generic;
3+
4+
namespace Umbraco.Forms.Integrations.Automation.Zapier.Validators
5+
{
6+
public abstract class WorkflowBaseValidator<T>
7+
where T : class
8+
{
9+
public abstract bool IsValid(T input, ref List<Exception> exceptions);
10+
}
11+
}
Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using System.Configuration;
4+
using System.Net.Http;
5+
6+
using Umbraco.Forms.Core;
7+
using Umbraco.Forms.Core.Enums;
8+
using Umbraco.Forms.Core.Persistence.Dtos;
9+
using Umbraco.Forms.Integrations.Automation.Zapier.Services;
10+
using Umbraco.Forms.Integrations.Automation.Zapier.Validators;
11+
using Umbraco.Web;
12+
13+
namespace Umbraco.Forms.Integrations.Automation.Zapier
14+
{
15+
public class ZapierWorkflow : WorkflowType
16+
{
17+
private const string UserGroup = "Umbraco.Cms.Integrations.Automation.Zapier.UserGroup";
18+
19+
private readonly IUmbracoContextAccessor _umbracoContextAccessor;
20+
21+
public ZapierWorkflow(IUmbracoContextAccessor umbracoContextAccessor)
22+
{
23+
_umbracoContextAccessor = umbracoContextAccessor;
24+
25+
Name = "Trigger Zap";
26+
Id = new Guid("d05b95e5-86f8-4c31-99b8-4ec7fc62a787");
27+
Description = "Automation workflow for triggering Zaps in Zapier.";
28+
Icon = "icon-tools";
29+
}
30+
31+
[Core.Attributes.Setting("Fields Mappings",
32+
Description = "Please map form information to the Zap ones.",
33+
View = "FieldMapper")]
34+
public string Mappings { get; set; }
35+
36+
[Core.Attributes.Setting("Standard Fields Mappings",
37+
Description = "Please map any standard form information to send.",
38+
View = "StandardFieldMapper")]
39+
public string StandardFieldsMappings { get; set; }
40+
41+
[Core.Attributes.Setting("WebHook Uri",
42+
Description = "Zapier WebHook URL",
43+
View = "TextField")]
44+
public string WebHookUri { get; set; }
45+
46+
// Using a static HttpClient (see: https://www.aspnetmonsters.com/2016/08/2016-08-27-httpclientwrong/).
47+
private readonly static HttpClient s_client = new HttpClient();
48+
49+
// Access to the client within the class is via ClientFactory(), allowing us to mock the responses in tests.
50+
public static Func<HttpClient> ClientFactory = () => s_client;
51+
52+
public override WorkflowExecutionStatus Execute(Record record, RecordEventArgs e)
53+
{
54+
try
55+
{
56+
IFieldMappingBuilder builder = new FieldMappingBuilder(_umbracoContextAccessor);
57+
58+
var content = builder
59+
.IncludeFieldsMappings(Mappings, e)
60+
.IncludeStandardFieldsMappings(StandardFieldsMappings, e)
61+
.Map();
62+
63+
var result = ClientFactory().PostAsync(WebHookUri, new FormUrlEncodedContent(content)).GetAwaiter().GetResult();
64+
65+
return result.IsSuccessStatusCode ? WorkflowExecutionStatus.Completed : WorkflowExecutionStatus.Failed;
66+
}
67+
catch (Exception exception)
68+
{
69+
Umbraco.Core.Composing.Current.Logger.Error(typeof(ZapierWorkflow), exception);
70+
71+
return WorkflowExecutionStatus.Failed;
72+
}
73+
}
74+
75+
public override List<Exception> ValidateSettings()
76+
{
77+
var exceptions = new List<Exception>();
78+
79+
var validator = new WebHookValidator();
80+
validator.IsValid(WebHookUri, ref exceptions);
81+
82+
if(string.IsNullOrEmpty(ConfigurationManager.AppSettings[UserGroup]))
83+
exceptions.Add(new Exception("User group setting is required."));
84+
85+
return exceptions;
86+
}
87+
88+
89+
}
90+
}
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
<?xml version="1.0" encoding="utf-8"?>
2+
<umbPackage>
3+
<info>
4+
<package>
5+
<name>Umbraco.Forms.Integrations.Automation.Zapier</name>
6+
<version>1.0.0</version>
7+
<iconUrl></iconUrl>
8+
<licence url="https://opensource.org/licenses/MIT">MIT</licence>
9+
<url>https://github.com/umbraco/Umbraco.Forms.Integrations</url>
10+
<requirements type="strict">
11+
<major>8</major>
12+
<minor>1</minor>
13+
<patch>0</patch>
14+
</requirements>
15+
</package>
16+
<author>
17+
<name>Umbraco HQ</name>
18+
<website>https://github.com/umbraco/Umbraco.Forms.Integrations</website>
19+
</author>
20+
<contributors>
21+
<contributor>Adrian Cojocariu</contributor>
22+
</contributors>
23+
<readme><![CDATA[An extension for Umbraco Forms adding support for calling Zapier triggers.]]></readme>
24+
</info>
25+
<files>
26+
<file path="bin/release/net472/Umbraco.Forms.Integrations.Automation.Zapier.dll" orgPath="bin/Umbraco.Forms.Integrations.Automation.Zapier.dll" />
27+
</files>
28+
</umbPackage>

0 commit comments

Comments
 (0)