Blogs from Episerver teams2023-10-25T04:10:08.0000000Z/product-blogs/the-add-ons-blog/Optimizely WorldVulnerability in EPiServer.Forms/blogs/phu-nguyen/dates/2023/10/vulnerability-in-episerver-forms/2023-10-25T04:10:08.0000000Z<p><strong>Introduction</strong><br />We recently fixed a potential security vulnerability for the Optimizely Forms addon, customers may face this issue with any Forms version, the problem will happen when using a CMS function without noticing its noted behaviors. It could lead to losing security protection for some of the end-users' data.</p>
<p><strong>Risk</strong><br />Overall, the risk of vulnerability is high, especially if your website uses content indexing services (like Find or other search engines).</p>
<p><strong>Mitigation </strong></p>
<p>The issue has been fixed in EPiServer.Forms <a href="https://nuget.optimizely.com/package/?id=EPiServer.Forms&v=5.7.0">v5.7.0</a> (<a href="/link/1933ba72787346df9003b7a4c7d1cff8.aspx?epsremainingpath=bug/AFORM-3620">AFORM-3620</a>)<span class="TrackChangeTextInsertion TrackedChange SCXW119116340 BCX0"><span class="TextRun SCXW119116340 BCX0"><span class="NormalTextRun SCXW119116340 BCX0"> for CMS 12 and <a href="https://nuget.optimizely.com/package/?id=EPiServer.Forms&v=4.31.0">v4.31.</a></span></span></span><span class="TrackChangeTextInsertion TrackedChange SCXW119116340 BCX0"><span class="TextRun SCXW119116340 BCX0"><span class="NormalTextRun SCXW119116340 BCX0"><a href="https://nuget.optimizely.com/package/?id=EPiServer.Forms&v=4.31.0">0</a> for </span></span></span><span class="TrackChangeTextInsertion TrackedChange SCXW119116340 BCX0"><span class="TextRun SCXW119116340 BCX0"><span class="NormalTextRun SCXW119116340 BCX0">CMS 11</span></span></span><span class="TrackChangeTextDeletionMarker TrackedChange SCXW119116340 BCX0"><span class="TextRun SCXW119116340 BCX0"><span class="NormalTextRun TrackChangeTextDeletion SCXW119116340 BCX0">. </span></span></span><span class="TrackChangeTextInsertion TrackedChange SCXW119116340 BCX0"><span class="TextRun SCXW119116340 BCX0"><span class="NormalTextRun SCXW119116340 BCX0">Please upgrade to those versions as soon as possible</span></span></span><span class="TrackChangeTextInsertion TrackedChange SCXW119116340 BCX0"><span class="TextRun SCXW119116340 BCX0"><span class="NormalTextRun SCXW119116340 BCX0">.</span></span></span></p>
<p><span style="text-decoration: underline;">For DXP service customers:</span></p>
<ul>
<li>Mitigation is in place for all DXP service customers.</li>
<li><em>Update (October 27)</em><span>: To clarify, we</span> have mitigated existing vulnerable vectors, but packages SHOULD be updated to mitigate the risk of reintroducing the vulnerability!</li>
</ul>
<p><strong>Affected versions <br /></strong><span class="TextRun SCXW229541896 BCX0"><span class="NormalTextRun SCXW229541896 BCX0">Any Forms version before 5.7.0 (CMS12) or Forms 4.31.0 (CMS11)</span><span class="NormalTextRun SCXW229541896 BCX0">.</span></span><span class="EOP SCXW229541896 BCX0"> </span></p>
<p><strong>Remediation</strong><br /><span class="NormalTextRun SCXW152076525 BCX0">If using the affected versions of </span><span class="NormalTextRun SpellingErrorV2Themed SCXW152076525 BCX0">EPiServer.Forms</span><span class="NormalTextRun SCXW152076525 BCX0"> listed above, please update to version 5.7.0 (CMS12) or Forms 4.31.0 (CMS11).</span></p>
<p>Please reach out to our support for further guidance by email to<span> </span><a href="mailto:support@optimizely.com">support@optimizely.com</a><span> </span>or submit a request at<span> </span><span><span class="ui-provider rw cvi ajm cvj cvk cvl cvm cvn cvo cvp cvq cvr cvs cvt cvu cvv cvw cvx cvy cvz cwa cwb cwc cwd cwe cwf cwg cwh cwi cwj cwk cwl cwm cwn cwo"><a href="https://support.optimizely.com/hc/en-us">https://support.optimizely.com/hc/en-us</a>.</span></span></p>
<h3>Questions</h3>
<p>If you have any questions, please contact our support team (with assistance from our <span>security engineering team)</span> at <a href="mailto:support@optimizely.com">support@optimizely.com</a><span>.</span></p>
<h3>Risk definitions</h3>
<p>Low – little to no potential impact on Optimizely or customer environments/data. Vulnerability has low exploitability, for example: requirement for local or physical system access, zero reachability to/executability within Optimizely products/code.</p>
<p>Medium – some potential impact on Optimizely or customer environments/data. Vulnerability has medium exploitability, for example: requirement to be located on the same local network as the target, requirement for an individual to be manipulated via social engineering, requirement for user privileges, vulnerability achieves limited access to Optimizely products/code.</p>
<p>High – high potential impact on Optimizely or customer environments/data. Vulnerability has high exploitability, for example: achieves high level access to Optimizely products/code, could elevate privileges, could result in a significant data loss or downtime.</p>
<p>Critical – very significant potential impact on Optimizely or customer environments/data. Vulnerability has very high exploitability, for example: achieves admin/root-level access to Optimizely products/code. Vulnerability does not require any special authentication credentials/knowledge of Optimizely products/environments.</p>The A/B testing addon for Optimizely CMS is now open-source/blogs/kevin-shea/dates/2022/8/the-ab-testing-addon-for-optimizely-cms-is-now-open-source/2022-08-05T17:47:56.0000000Z<p><span>Today we are announcing that we have open-sourced the A/B testing addon for </span><a href="https://www.optimizely.com/products/dxp/">Optimizely CMS 11</a> and <a href="https://github.com/episerver/content-ab-testing" target="_blank" rel="noopener">Optimizely CMS 12</a><span>.</span><span> </span></p>
<p><span>For several years, this tool has served as a springboard to the world of experimentation for CMS users and developers. That thrills us. Experimentation is a foundational pillar of Optimizely’s ethos and, in that spirit, we feel it’s important to maximize our focus on innovating in the areas that present the most effective results for our customers. </span><span> </span></p>
<p><span>Optimizely’s </span><a href="https://www.optimizely.com/products/intelligence/web-experimentation/"><span>Web and Full Stack</span></a><span> experimentation platforms provide the most advanced and robust experimentation capabilities on the market. It enables users with an immensely powerful set of tools for managing modern, dynamic, application experiences. It is also the platform upon which Optimizely will pursue its future innovation in the space.</span><span> </span></p>
<p><span>As we wind down our efforts on the addon, two factors standout:</span><span> </span></p>
<ul>
<li><span>There are users that rely on the capabilities of the tool today.</span></li>
<li>The developer community has historically expressed interest in extending, modifying, or replacing capabilities within the tool. </li>
</ul>
<p><span>This makes the addon an ideal candidate to open to continue life as a community supported project.</span><span> </span></p>
<p><span>What happens next?</span></p>
<ul>
<li><span>The source for the addon is now open and available at </span><a href="https://github.com/episerver/ab-testing"><span>episerver/ab-testing (github.com)</span></a><span>. Fork the repository to add, update, or replace capabilities as it’s relevant to your projects.</span></li>
<li><span>Optimizely will continue to actively maintain support for the addon for 3 months, ending on November 15, 2022.</span></li>
<li>After November 15, 2022, Optimizely’s official contributions to the project will come to an end. The source for the addon will remain available to the community and free to use or modify as you see fit.</li>
</ul>
<p><span>Happy experimenting!</span></p>
<p><strong>EDIT:</strong> This article was updated to include a link to repository for the CMS 12 version of the add-on, in addition to the CMS 11 version.</p>Introducing: Episerver PDF Preview add-on/blogs/ninh-doan/dates/2019/4/pdfviewer-introduction/2019-04-22T08:47:02.0000000Z<p>The <a href="/link/f7f020d815674508bd7fdc247e0d837b.aspx">Episerver PDF Preview</a> is the latest add-on that allows editors to preview PDF documents in edit view. </p>
<p>This add-on is based on <a href="https://mozilla.github.io/pdf.js/"><span class="caps">PDF</span>.js</a> version 2.0.943. </p>
<h2>Intergration with your current system</h2>
<p><span>In the </span><strong>PdfPreview</strong><span> package, there is a class named </span><span class="classLib">PdfFile</span><span> which handles uploaded files with the .pdf extension. </span></p>
<h3>1. The system does not have a model for handling PDF files</h3>
<p>If the system does not have a model for handling PDF files, the default<span> </span><span class="classLib">PdfFile</span><span> </span>model provided by the package is used. Editors can now preview PDF files in edit view without any extra steps.</p>
<h3>2. The system already has a model for handling PDF files</h3>
<p>By default, the<span> </span><strong><em><span class="classLib">ContentMediaResolver</span> </em></strong>class from Episerver CMS Core gets the first matching media implementation type registered for an extension. If there is an existing type registered for “pdf”, the<span> </span><span class="classLib">PdfFile</span><span> </span>media implementation in the Episerver package is used, but in this case the existing type should be chosen.</p>
<p>To change this default behavior, a media resolver class named<span> </span><strong><em><span class="classLib">PdfContentMediaResolver</span> </em></strong>is used to ignore the<span> </span><span class="classLib">PdfFile</span><span> </span>type in the Episerver package and thus, the existing registered media type is the candidate.</p>
<p><span class="classLib">PdfContentMediaResolver</span><span> </span>inherits<span> </span><span class="classLib">ContentMediaResolver</span><span> </span>and overrides the<span> </span><em><span class="classLib">Type GetFirstMatching(string extension) </span></em>method to handle the business logic above. </p>
<p>To turn on the PDF preview, the PDF media model must implement the<span> </span><em><span class="classLib">IPdfFile</span> </em>interface. For example:</p>
<pre class="language-csharp"><code>[ContentType(DisplayName = "PdfFile", GUID = "…", Description = "")]
[MediaDescriptor(ExtensionString = "pdf")]
public class PdfFile : MediaData, IPdfFile
{
}</code></pre>
<h3>Note: </h3>
<p><em><span class="classLib">PdfContentMediaResolver</span></em><em> </em>will ignore the<span> </span><span class="classLib">PdfFile</span><span> </span>type in the Episerver package when system has another type registered for “pdf”, no matter it is created before or after install package. If it is created after install package, AppPool restart is required to r<span>eflect the changes.</span></p>
<p><strong>Demo:</strong></p>
<ul>
<li>Before: </li>
</ul>
<p><img src="/link/c821c71eb59146129d041db7e3fe8521.aspx" /></p>
<ul>
<li>After:</li>
</ul>
<p><img src="/link/ae61d5e784024a359cacb7816a22fc5f.aspx" /></p>
<p> </p>Auto Translation V2 to V3 migration/blogs/yen-nguyen/dates/2019/3/auto-translation-api-v3-was-released/2019-03-26T03:46:39.0000000Z<h2><br />Api V3 has came out.</h2>
<p>Microsoft has migrated auto translation service api from V2 to V3 in the beginning of this year. <br />Version 2 was deprecated on April 30, 2018 and will be discontinued on April 30, 2019 according to Microsoft announcement <a href="https://docs.microsoft.com/en-us/azure/cognitive-services/translator/migrate-to-v3" title="V2 to V3 Migration" target="_blank" rel="noopener">here</a><br />Episerver's Language Manager add-on was updated in response to that and released in version EPiServer.Labs.LanguageManager 3.3.0. <br />The upgraded auto translation service's name is changed to "Cognitive Service Translation" (was "Bing Translator" before) with new api endpoint: <strong>api.cognitive.microsofttranslator.com</strong>.<br />Microsoft also introduces service improvements in V3:</p>
<ul>
<li>Is generally available and fully supported.</li>
<li>Is GDPR compliant as a processor and satisfies all ISO 20001 and 20018 as well as SOC 3 certification requirements.</li>
<li>Allows you to invoke the neural network translation systems you have customized with Custom Translator (Preview), the new Translator NMT customization feature.</li>
<li>Does not provide access to custom translation systems created using the Microsoft Translator Hub.<br /><br /></li>
</ul>
<h2>Authentication</h2>
<p>The V3 api will accept authentication key of V2, so you don't need to get a new subscription that make it easier for you to migrate to new version without breaking anything.</p>
<h2>How to update Episerver's Language Manager package?</h2>
<p>These updates were released in package EPiServer.Labs.LanguageManager 3.3.0. So, all you need to do is to update this package, then go to Language Manager settings change provider to "Cognitive Service Translation".<br />All behaviors should remain the same.</p>
<p><img src="/link/81aaa3538934467498694335ba22c0d6.aspx" width="500" height="298" alt="" /></p>
<p><br />Hope you guys find this info useful. Thanks!</p>EPiServer.Forms create custom Action for Field Dependency /blogs/quan-tran/dates/2019/1/episerver-forms-create-custom-action-for-field-dependency-/2019-02-20T08:51:56.0000000Z<p>Feature dependency for fields is available from version 4.15 which lets you configure dependencies among fields in a form. You can create rules for field elements on a new Dependencies tab in the element properties. There are two buit-in actions Show and Hide by default.</p>
<p>In this post, I'm going to implement a simple custom action that can make the fields change its background color based on value of other field(s).</p>
<ol>
<li>Create class name <strong>RedBackgroundColorAction</strong> inherit from <strong>IDependencyAction</strong></li>
</ol>
<pre class="language-csharp"><code>using EPiServer.Forms.Core.Internal.Dependency;
using EPiServer.ServiceLocation;
namespace CustomFieldDependency
{
/// <summary>
/// New Action for form field dependency. Set the background color to form element.
/// </summary>
[ServiceConfiguration(typeof(IDependencyAction))]
public class RedBackgroundColorAction : IDependencyAction
{
/// <summary>
/// Display name of the action, show to Editor
/// </summary>
public string DisplayName => "In red background";
/// <summary>
/// Order in which the action will be listed in the dropdown for Editor
/// </summary>
public int Order => 3;
/// <summary>
/// Name of the action. This should be unique among others
/// </summary>
public string Name => GetType().FullName;
/// <summary>
/// Name of the method at clientside (Javascript), which will be called when dependency state changes
/// </summary>
public string ClientsideAction => "RedBackgroundColorAction";
}
}</code></pre>
<p>Let's rebuild your project, create a form with two fields: Text and Textarea. On Dependencies tab of Textarea element, the new action is listed. Select the newly created <em>In Red Background</em> action. The background color of Textarea element will be changed to red if the value of Text element contains <em>episerver</em></p>
<p><em><img src="/link/e1da4ac0798b4ed889b96933a7142321.aspx" /></em></p>
<p>2. We need to implement the action <strong>RedBackgroundColorAction</strong> on clide side. Create a file name <em>FormFieldDependency.js</em> and put it under <em>~/ClientResources/Scripts</em> folder</p>
<pre class="language-javascript"><code>(function ($) {
$.extend(true, epi.EPiServer.Forms.Dependency.Actions, {
//extending Actions for Form Field Dependency
RedBackgroundColorAction: function ( /*epi.EPiServer.Forms.Dependency.DependantController*/ controller) {
var elementName = controller.dependantInfo.fieldName;
var $wrapperElement = $('[data-f-element-name="' + elementName + '"]', controller.workingFormInfo.$workingForm);
var $inputElement = $('[data-f-datainput]', $wrapperElement); // or simply controller.$domElement
var dependencyInfo = getDependencyInfo(controller.workingFormInfo, controller.dependantInfo.fieldName); // or simply controller.dependantInfo
if (!dependencyInfo) {
return;
}
if (controller.isSatisfied) {
$inputElement.addClass('bg-red');
} else {
$inputElement.removeClass('bg-red');
}
}
});
// get dependency infor of a field
function getDependencyInfo(workingFormInfo, fieldName) {
var dependencies = workingFormInfo.DependenciesInfo;
if (!dependencies || dependencies.length === 0) {
return null;
}
for (var i = 0; i < dependencies.length; i++) {
if (dependencies[i].fieldName === fieldName) {
return dependencies[i];
}
}
return null;
}
})($$epiforms || $);</code></pre>
<p>3. Create <em>FormFieldDependency.css</em> file to write cutom style for elements and put it under <em>~/ClientResources/Styles</em> folder</p>
<pre class="language-css"><code>.bg-red {
background-color:red !important;
}</code></pre>
<p>4. Register external client resources to EPiServer.Forms. </p>
<pre class="language-csharp"><code>using System;
using System.Collections.Generic;
using EPiServer.Forms.Implementation;
using EPiServer.ServiceLocation;
namespace CustomFieldDependency
{
/// <summary>
/// Register client resources for EPiServer.Forms
/// </summary>
[ServiceConfiguration(ServiceType = typeof(IViewModeExternalResources))]
public class ViewModeExternalResources : IViewModeExternalResources
{
public virtual IEnumerable<Tuple<string, string>> Resources
{
get
{
var arrRes = new List<Tuple<string, string>>();
arrRes.Add(new Tuple<string, string>("script", "/ClientResources/Scripts/FormFieldDependency.js"));
arrRes.Add(new Tuple<string, string>("css", "/ClientResources/Styles/FormFieldDependency.css"));
return arrRes;
}
}
}
}</code></pre>
<p>Let's view the form in Viewmode, enter <em>episerver</em> in Text element and see the result.</p>
<p><img src="/link/4cf5bac336c04186b38b86f4fc74261d.aspx" /></p>
<p>Hope this help.</p>Implement a custom geolocation provider with Forms/blogs/yen-nguyen/dates/2019/1/implement-a-custom-geolocation-provider-/2019-01-04T05:44:30.0000000Z<p>I got a support ticket a few days ago in terms of Forms' hidden vistor profiling element. The customer basically wanted to capture user's zip code by using the element with a external geolocation api. He could get ip, location name however zip code always return empty in submission. So, i digged in the form's visitor profiling element and its geolocation provider. It turns out Form is using an api named FreeGeo and fetch geolocation info base on user's ip address and you can replace it by your own api for sure. The thing is, to make it work, we'll need some extra steps to config custom provider and justify the api result to meet our needs. The following implementaion is built with Episerver 11 Alloy site together with Forms 4.20. This is step by step guide to acomplish it. </p>
<h3><strong>1. Change configuration using new provider</strong><strong></strong></h3>
<p>Open Web.config file, add following block of config under <virtualPathProviders> section.</p>
<pre class="language-markup"><code> <geolocation defaultProvider="customGeoProvider">
<providers>
<add name="customGeoProvider" type="AlloySample.Internal.GeoData.CustomGeolocationProvider, AlloySample" geoApiUrl="http://{YOUR_API_URI}/{0}?access_key={YOUR_API_KEY}"/>
</providers>
</geolocation></code></pre>
<p>This config will switch geolocation to your custom one. You can modify type and geoApiUrl to fit your needs and notice that {0} will be filled with user IP when making a request to api.</p>
<h3><strong>2. Create custom provider to fetch api result</strong><strong></strong></h3>
<p>Firstly, we add a new provider class that inherit GeolocationProviderBase class and ICustomGeolocationProvider interface (this is required because Forms will scan all implementation of ICustomGeolocationProvider to process). In this sample, i created a provider class named CustomGeolocaitonProvider</p>
<pre class="language-csharp"><code> public class CustomGeolocationProvider : GeolocationProviderBase, ICustomGeolocationProvider
{
private string _geoApiUrl { get; set; }
private static readonly ILogger _logger = LogManager.GetLogger(typeof(FreeGeolocationProvider));
public override IGeolocationResult Lookup(System.Net.IPAddress address)
{
return GetGeoData(address.ToString()).Result;
}
private async Task<EPiServer.Forms.Internal.GeoData.GeolocationResult> GetGeoData(string clientIp)
{
if (string.IsNullOrEmpty(clientIp))
{
return null;
}
//create request and get reponse in Json from API
var request = (HttpWebRequest)WebRequest.Create(string.Format(_geoApiUrl, clientIp));
request.Method = WebRequestMethods.Http.Get;
request.Accept = "application/json";
var response = await request.GetResponseAsync().ConfigureAwait(false);
string jsonResponse = "";
using (var reader = new StreamReader(response.GetResponseStream()))
{
jsonResponse = reader.ReadToEnd();
}
if (string.IsNullOrEmpty(jsonResponse))
{
return null;
}
EPiServer.Forms.Internal.GeoData.GeolocationResult result;
try
{
//convert json response to GeolocationResult
dynamic geoJsonData = jsonResponse.ToObject();
result = new EPiServer.Forms.Internal.GeoData.GeolocationResult
{
ip = geoJsonData["ip"],
country_code = geoJsonData["country_code"],
country_name = geoJsonData["country_name"],
region_code = geoJsonData["region_code"],
region_name = geoJsonData["region_name"],
city = geoJsonData["city"],
zip_code = geoJsonData["zip"],
time_zone = geoJsonData["time_zone"]?.id,
latitude = geoJsonData["latitude"],
longitude = geoJsonData["longitude"],
CountryCode = geoJsonData["country_code"],
Region = geoJsonData["region_name"],
};
}
//if jsonResponse is not a valid json string:
catch
{
_logger.Error("Free Geo Api does not return a valid json string");
return null;
}
return result;
}
public override IEnumerable<string> GetCountryCodes(string continentCode)
{
throw new NotImplementedException();
}
public override IEnumerable<string> GetRegions(string countryCode)
{
throw new NotImplementedException();
}
public override void Initialize(string name, System.Collections.Specialized.NameValueCollection config)
{
base.Initialize(name, config);
//get geoApiUrl in Provider Config
if (string.IsNullOrEmpty(config["geoApiUrl"]))
{
throw new ConfigurationErrorsException("Invalid configuration. Geo API url is missing");
}
_geoApiUrl = config["geoApiUrl"];
}
public override Capabilities Capabilities => throw new NotImplementedException();
public IGeoDataProcessService GeoDataProcessService
{
get
{
return new CustomGeoDataProcessService();
}
}
}</code></pre>
<p>In this provider class, we care about two methods: GetGeoData - method will handle geolocation data receiving and GeoDataProcessService - method return data processor. In GetGeoData method, just go ahead and write your code to pull api geo location data. I'm assuming your api is returning json data, the code is nothing but a simple api call using WebRequest. </p>
<pre class="language-csharp"><code> var request = (HttpWebRequest)WebRequest.Create(string.Format(_geoApiUrl, clientIp));
request.Method = WebRequestMethods.Http.Get;
request.Accept = "application/json";
var response = await request.GetResponseAsync().ConfigureAwait(false);
string jsonResponse = "";
using (var reader = new StreamReader(response.GetResponseStream()))
{
jsonResponse = reader.ReadToEnd();
}
if (string.IsNullOrEmpty(jsonResponse))
{
return null;
}</code></pre>
<p>After receving json response from api, the remaining task is so simple, you just need to create a new GeoLocationResult instance to store all json data by mapping field by field of json to result model or you can custome result here. The tricky point is that you need to map result property value to the right json object property. For example, my api return json result with "zip": "XXX" but the other api return "zip_code": "XXX".</p>
<pre class="language-csharp"><code> try
{
//convert json response to GeolocationResult
dynamic geoJsonData = jsonResponse.ToObject();
result = new EPiServer.Forms.Internal.GeoData.GeolocationResult
{
ip = geoJsonData["ip"],
country_code = geoJsonData["country_code"],
country_name = geoJsonData["country_name"],
region_code = geoJsonData["region_code"],
region_name = geoJsonData["region_name"],
city = geoJsonData["city"],
zip_code = geoJsonData["zip"],
time_zone = geoJsonData["time_zone"]?.id,
latitude = geoJsonData["latitude"],
longitude = geoJsonData["longitude"],
CountryCode = geoJsonData["country_code"],
Region = geoJsonData["region_name"],
};
}
//if jsonResponse is not a valid json string:
catch
{
_logger.Error("Free Geo Api does not return a valid json string");
return null;
}</code></pre>
<p>After get geo data, we'll need one more step to process the data. Method GeoDataProcessService return a service named GeoDataProcessService that will be created on the next step.</p>
<h3><strong>3. Add a custom geolocation data process service </strong></h3>
<p>Add a new class, name it whatever you want, inherit IGeoDataProcessService with code as below:</p>
<pre class="language-csharp"><code> public class CustomGeoDataProcessService : IGeoDataProcessService
{
EPiServer.Forms.Internal.GeoData.GeolocationResult IGeoDataProcessService.ProcessGeoData(IGeolocationResult geoLocationResult)
{
var result = geoLocationResult as EPiServer.Forms.Internal.GeoData.GeolocationResult;
return result;
}
}</code></pre>
<p>This class has only one method and it gets IGeolocationResult from provider's Lookup method and return strong typed result. This step is mandatory because Forms requires a data processor.</p>
<p>Compile, run and subit forms, im now able to capture geolocation info as exact as api result. </p>
<p><img src="/link/9e29603508f74447acad69fae9ab0631.aspx" width="440" height="177" /></p>
<p>*Note: The form's element will work only if site is online, the local sites won't be able to capture gelocation data.</p>
<p><span>If you have any questions in relation to this post, please comment below. Thanks!</span></p>Display all Opt-in processes in EPiServer Campaign Connector/blogs/nhat-luu/dates/2018/10/display-all-opt-in-processes-in-episerver-campaign-connector/2018-10-12T08:27:16.0000000Z<p>By default, EPiServer Campaign Connector only displays opt-in processes of type <strong>Double</strong>: </p>
<p><img src="/link/c869bf89c83443f49d002aa0ce6c0424.aspx" /></p>
<p>However, sometimes we want to display opt-in processes of other types (<strong>Single</strong><strong>, Confirmed</strong>). We can easily achieve this by overriding the default behaviour.</p>
<p>Firstly, we need to create a class which inherits from the default OptinProcesseService, then override the <strong>GetAllowedOptInProcesses </strong>method:</p>
<pre class="language-csharp"><code>public class CustomOptinProcessServive : OptinProcessService
{
...
public override IEnumerable<SelectItem> GetAllowedOptInProcesses()
{
// GetAllOptInProcesses() returns a list of all opt-in processes where each item is a Tuple<long, string, string>:
// + Item1 is the Id of the optin process
// + Item2 is the Name of the optin process
// + Item3 is the Type of the optin process
return GetAllOptInProcesses().Select(x => new SelectItem() { Text = x.Item2, Value = x.Item1 });
}
...
}</code></pre>
<p>Then register the above class as the default implementation for <strong>IOptinProcessService</strong>:</p>
<pre class="language-csharp"><code>context.ConfigurationComplete += (o, e) =>
{
context.Services.AddTransient<IOptinProcessService, CustomOptinProcessServive>();
};</code></pre>
<p>Now the Opt-in process drop-down will display all opt-in processes:</p>
<p><img src="/link/d0ed9912189e40c1b477bc883bab7f2c.aspx" /></p>Implement search provider for pages, blocks and media/blogs/le-giang/dates/2018/4/search-for-pages-blocks-and-media-in-navigationtree/2018-04-02T03:50:21.0000000Z<p>Currently when I type to search in navigationtree, surprisingly there is no result returned even for pages, block or media. It seems that someone forgot to implement searching feature or it is being underplayed <img alt="smile" src="/Scripts/tinymce/plugins/emoticons/img/smiley-smile.gif" />.</p>
<p>In this post, I would like show you how to implement a search provider for pages, blocks and media and you can customize to fit your need.</p>
<p>For pages:</p>
<pre class="language-csharp"><code>[SearchProvider]
public class DefaultPageSearchProvider : ContentSearchProviderBase<IContent, ContentType>
{
public DefaultPageSearchProvider(LocalizationService localizationService, ISiteDefinitionResolver siteDefinitionResolver, IContentTypeRepository<ContentType> contentTypeRepository, EditUrlResolver editUrlResolver, ServiceAccessor<SiteDefinition> currentSiteDefinition, LanguageResolver languageResolver, UrlResolver urlResolver, TemplateResolver templateResolver, UIDescriptorRegistry uiDescriptorRegistry) : base(localizationService, siteDefinitionResolver, contentTypeRepository, editUrlResolver, currentSiteDefinition, languageResolver, urlResolver, templateResolver, uiDescriptorRegistry)
{
}
/// <summary>
///
/// </summary>
public override string Area
{
get
{
return "cms/pages";
}
}
/// <summary>
///
/// </summary>
public override string Category
{
get
{
return "pages";
}
}
protected override string IconCssClass
{
get
{
return "epi-iconObjectPage";
}
}
public override IEnumerable<SearchResult> Search(Query query)
{
var contentRepository = ServiceLocator.Current.GetInstance<IContentRepository>();
var pageQueryService = ServiceLocator.Current.GetInstance<IPageCriteriaQueryable>();
PropertyCriteriaCollection crits = new PropertyCriteriaCollection();
PropertyCriteria nameCriteria = new PropertyCriteria();
nameCriteria.Name = MetaDataProperties.PageName;
nameCriteria.Value = query.SearchQuery;
nameCriteria.Type = PropertyDataType.String;
nameCriteria.Required = true;
nameCriteria.Condition = CompareCondition.Contained;
crits.Add(nameCriteria);
//Add criteria so search is performed against all providers
crits.Add(new PropertyCriteria
{
Name = "EPI:MultipleSearch",
Value = "*"
});
PageDataCollection pages = null;
try
{
pages = pageQueryService.FindAllPagesWithCriteria(ContentReference.RootPage, crits, null, LanguageSelector.MasterLanguage());
}
catch (NotImplementedException)
{
// If the provider hasn't implemented FindAllPagesWithCriteria, call old FindPagesWithCriteria instead.
pages = pageQueryService.FindPagesWithCriteria(ContentReference.RootPage, crits, null, LanguageSelector.MasterLanguage());
}
return pages.Select(CreateSearchResult);
}
}</code></pre>
<p><strong>For blocks</strong></p>
<pre class="language-csharp"><code> [SearchProvider]
public class DefaultBlockSearchProvider : ContentSearchProviderBase<IContent, ContentType>
{
public DefaultBlockSearchProvider(LocalizationService localizationService, ISiteDefinitionResolver siteDefinitionResolver, IContentTypeRepository<ContentType> contentTypeRepository, EditUrlResolver editUrlResolver, ServiceAccessor<SiteDefinition> currentSiteDefinition, LanguageResolver languageResolver, UrlResolver urlResolver, TemplateResolver templateResolver, UIDescriptorRegistry uiDescriptorRegistry) : base(localizationService, siteDefinitionResolver, contentTypeRepository, editUrlResolver, currentSiteDefinition, languageResolver, urlResolver, templateResolver, uiDescriptorRegistry)
{
}
/// <summary>
///
/// </summary>
public override string Area
{
get
{
return "cms/blocks";
}
}
/// <summary>
///
/// </summary>
public override string Category
{
get
{
return "blocks";
}
}
protected override string IconCssClass
{
get
{
return "epi-objectIcon";
}
}
public override IEnumerable<SearchResult> Search(Query query)
{
ContentReference root = null;
if (query.SearchRoots != null && query.SearchRoots.Count() > 0)
{
root = new ContentReference(query.SearchRoots.First());
}
else
{
root = new ContentReference();
}
var contentRepository = ServiceLocator.Current.GetInstance<IContentRepository>();
var contentTypeRepository = ServiceLocator.Current.GetInstance<IContentTypeRepository>();
var contentProviderManager = ServiceLocator.Current.GetInstance<IContentProviderManager>();
var blockTypes = contentTypeRepository.List().Where(x => typeof(BlockData).IsAssignableFrom(x.ModelType));
var provider = contentProviderManager.GetProvider(root);
if (provider != null && provider.HasCapability(ContentProviderCapabilities.Search))
{
List<IContent> blocks = new List<IContent>();
foreach(var blockType in blockTypes)
{
blocks.AddRange(provider.ListContentOfContentType(blockType).Where(x => x.Name.IndexOf(query.SearchQuery, StringComparison.InvariantCultureIgnoreCase) >= 0).Select(x => contentRepository.Get<IContent>(x.ContentLink)));
}
return blocks.Select(CreateSearchResult);
}
return Enumerable.Empty<SearchResult>();
}
}</code></pre>
<p>For media:</p>
<pre class="language-csharp"><code>[SearchProvider]
public class DefaultMediaSearchProvider : ContentSearchProviderBase<IContent, ContentType>
{
public DefaultMediaSearchProvider(LocalizationService localizationService,
ISiteDefinitionResolver siteDefinitionResolver,
IContentTypeRepository<ContentType> contentTypeRepository,
EditUrlResolver editUrlResolver,
ServiceAccessor<SiteDefinition> currentSiteDefinition,
LanguageResolver languageResolver,
UrlResolver urlResolver,
TemplateResolver templateResolver,
UIDescriptorRegistry uiDescriptorRegistry) : base(localizationService,
siteDefinitionResolver,
contentTypeRepository,
editUrlResolver,
currentSiteDefinition,
languageResolver,
urlResolver,
templateResolver,
uiDescriptorRegistry)
{
}
/// <summary>
///
/// </summary>
public override string Area
{
get
{
return "CMS/files";
}
}
/// <summary>
///
/// </summary>
public override string Category
{
get
{
return "media";
}
}
protected override string IconCssClass
{
get
{
return "";
}
}
public override IEnumerable<SearchResult> Search(Query query)
{
ContentReference root = null;
if (query.SearchRoots != null && query.SearchRoots.Count() > 0)
{
root = new ContentReference(query.SearchRoots.First());
}
else
{
root = new ContentReference();
}
var contentRepository = ServiceLocator.Current.GetInstance<IContentRepository>();
var contentTypeRepository = ServiceLocator.Current.GetInstance<IContentTypeRepository>();
var contentProviderManager = ServiceLocator.Current.GetInstance<IContentProviderManager>();
var mediaTypes = contentTypeRepository.List().Where(x => typeof(MediaData).IsAssignableFrom(x.ModelType));
var provider = contentProviderManager.GetProvider(root);
if (provider != null && provider.HasCapability(ContentProviderCapabilities.Search))
{
List<IContent> mediaList = new List<IContent>();
foreach (var blockType in mediaTypes)
{
mediaList.AddRange(provider.ListContentOfContentType(blockType).Where(x => x.Name.IndexOf(query.SearchQuery, StringComparison.InvariantCultureIgnoreCase) >= 0).Select(x => contentRepository.Get<IContent>(x.ContentLink)));
}
return mediaList.Select(CreateSearchResult);
}
return Enumerable.Empty<SearchResult>();
}
}</code></pre>
<p>After adding these search providers, now I can enjoy the filtering feature. You can customize the code to fit your need and improve the performance (for example: implement caching mechanism).</p>
<p><video width="100%" height="auto" src="/link/d17abf0512a94dbdb17a1d2646efcdd3.aspx" preload="none" controls="controls"><object width="0" height="240" data="https://world.episerver.com/epiui/EPiServer.Cms.TinyMce/1.0.0/ClientResources/tinymce/plugins/media/moxieplayer.swf" type="application/x-shockwave-flash"><param name="src" value="https://world.episerver.com/epiui/EPiServer.Cms.TinyMce/1.0.0/ClientResources/tinymce/plugins/media/moxieplayer.swf" /><param name="flashvars" value="url=/globalassets/cms-searching.mp4&poster=/epiui/CMS/" /><param name="allowfullscreen" value="true" /><param name="allowscriptaccess" value="true" /></object></video></p>Upload option in the Select content dialog/blogs/le-giang/dates/2018/3/upload-option-in-the-select-content-dialog/2018-03-06T11:10:45.0000000Z<p>Hi again!</p>
<p>When working with media contents (image, video...) in the CMS, you can only upload your files in the Media component. In this post I would like to introduce an add-on which allows you to upload media file when you are in the Select content dialog. Let's see it in action.</p>
<p><video width="100%" height="auto" controls="controls" autoplay="autoplay" preload="none" src="/link/cbb2a92c69f449638606baffc890754d.aspx"></video></p>
<p>I have also added this function to the TinyMCE editor, let's see it below.</p>
<p><video width="100%" height="auto" controls="controls" autoplay="autoplay" preload="none" src="/link/fed5966caac3433b9e2ce792e1c12f74.aspx"></video></p>
<p>Basically, it works as same as when you working in Media component but has some notable points.</p>
<ol>
<li>Upload button only get enabled when you select folder node on the tree.</li>
<li>OK button only get enabled when you select actual media content.</li>
<li>There is a sync between Select content dialog and Media component, this means that you can see uploaded files immediately in Media component after the uploading process completes in the Select content dialog.</li>
</ol>
<p>It took pretty much code so I have created a nuget package for this feature, but currently it only supports Episerver CMS 10. I will make it work with Episerver CMS 11 soon.</p>
<p><a title="EpiserverSite.TinyMCEFileBrowserPlugin.1.0.0-developerbuild.nupkg" href="https://drive.google.com/open?id=1xUVtQAAWTtIOJu6wSPTZVPNtBhPkb7zs">EpiserverSite.TinyMCEFileBrowserPlugin.1.0.0-developerbuild.nupkg</a></p>Custom FieldSet emelent block for EPiServer.Form/blogs/le-giang/dates/2018/2/custom-fieldset-emelent-block-for-episerver-form/2018-02-08T09:17:24.0000000Z<!DOCTYPE html>
<html>
<head>
</head>
<body>
<p>Hi there.</p>
<p>In this post, I will show you how to create a custom field set element block for FORM addon. This block aims to group some of form elements and add some pretty appearances for the form. And this is what we will achieve.</p>
<p><img src="/link/9860ad0e575f41dbb6ed87553527e509.aspx" alt="Image custom_form_1.png" /><img src="/link/fbd544900b7d4f9ca3ea9c3a16937fd4.aspx" alt="Image custom_form_submissions_1.png" /></p>
<ol>
<li>Create a new form element block called "FieldsetBlock" which cotains a element area allow user to drag and drop other form elements into. I also introduce a new interface called "IFieldSet", we will use this for some additional logic later.
<pre class="language-csharp"><code>[ContentType(GUID = "2A4F56F9-2D3A-4C0A-AD78-1381D6A57808", GroupName = "Layout", DisplayName = "Field set")]
public class FieldsetBlock: ElementBlockBase, IFieldSet, IExcludeInSubmission
{
[AllowedTypes(new[] { typeof(DataElementBlockBase) })]
[Display(GroupName = "Elements")]
[UIHint("FormsContentAreaEditor")]
public virtual ContentArea ElementsArea { get; set; }
public virtual string Title { get; set; }
}
public interface IFieldSet
{
ContentArea ElementsArea { get; set; }
}</code></pre>
</li>
<li>Create a view for the <span><span>FieldsetBlock</span></span>
<pre class="language-html"><code>@model EpiserverSite2.Models.FormElement.FieldsetBlock
<div class="Form__Element fieldsetBlock">
<fieldset>
<legend class="fieldset_title" @Html.EditAttributes(x => x.Title)>@Model.Title</legend>
@Html.PropertyFor(x => x.ElementsArea)
</fieldset>
</div> </code></pre>
</li>
<li>By the default, form elements have no on-page edit view, but the field set element need one. Add this code to restore the on-page edit view for the f<span><span>ield set element:</span></span>
<pre class="language-csharp"><code>[UIDescriptorRegistration]
public class FieldsetBlockUIDescriptor: UIDescriptor<FieldsetBlock>
{
public FieldsetBlockUIDescriptor()
{
DisabledViews = new List<string>();
}
}</code></pre>
<span><br /></span></li>
<li>We need to override the way the FORM retrieves form elements to include "sub elements" contained in the field set blocks. We have to because by the default, FORM only retrieves "first-level" elements.
<pre class="language-csharp"><code>[ServiceConfiguration(ServiceType = typeof(DataSubmissionService), Lifecycle = ServiceInstanceScope.Singleton)]
public class CustomDataSubmissionService : DataSubmissionService
{
protected override IFormStep GetCurrentStep(NameValueCollection rawSubmittedData, FormContainerBlock formContainer)
{
var step = base.GetCurrentStep(rawSubmittedData, formContainer);
var lang = (formContainer as ILocale).Language.Name;
var elements = new List<IFormElement>(step.Elements);
foreach (var el in step.Elements)
{
if (el.SourceContent is IFieldSet)
{
var allElements = (el.SourceContent as IFieldSet).ElementsArea.Items;
foreach (var item in allElements)
{
var formElementBlock = item.ContentLink.GetContent(lang) as ElementBlockBase;
if (formElementBlock != null && !elements.Contains(formElementBlock.FormElement))
{
elements.Add(formElementBlock.FormElement);
}
}
}
}
step.Elements = elements;
return step;
}
}</code></pre>
<pre class="language-csharp"><code> [ServiceConfiguration(ServiceType = typeof(FormBusinessService), Lifecycle = ServiceInstanceScope.Singleton)]
public class CustomFormBusinessService: FormBusinessService
{
public override IEnumerable<ElementBlockBase> GetDisplayableFormElementBlocks(FormContainerBlock formContainerBlock)
{
var allElements = GetFormElementBlocks(formContainerBlock, false);
var elementList = new List<ElementBlockBase>(allElements);
var lang = (formContainerBlock as ILocale).Language.Name;
foreach (var el in allElements)
{
if (el is IFieldSet)
{
var allSubElements = (el as IFieldSet).ElementsArea?.Items;
if (allSubElements != null && allSubElements.Count > 0)
{
foreach (var item in allSubElements)
{
var formElementBlock = item.ContentLink.GetContent(lang) as ElementBlockBase;
if (formElementBlock != null && !elementList.Contains(formElementBlock))
{
elementList.Add(formElementBlock);
}
}
}
}
}
return elementList;
}
/// <summary>
/// Return all FormElementBlock of this FomrContainer, regardless the publish, visibility status or personalization content condition.
/// </summary>
/// <returns></returns>
public override IEnumerable<ElementBlockBase> GetAllInnerFormElementBlocks(FormContainerBlock formContainerBlock)
{
var allElements = GetFormElementBlocks(formContainerBlock, true);
var elementList = new List<ElementBlockBase>(allElements);
var lang = (formContainerBlock as ILocale).Language.Name;
foreach (var el in allElements)
{
if (el is IFieldSet)
{
var allSubElements = (el as IFieldSet).ElementsArea?.Items;
if (allSubElements != null && allSubElements.Count > 0)
{
foreach (var item in allSubElements)
{
var formElementBlock = item.ContentLink.GetContent(lang) as ElementBlockBase;
if (formElementBlock != null && !elementList.Contains(formElementBlock))
{
elementList.Add(formElementBlock);
}
}
}
}
}
return elementList;
}
}</code></pre>
<p><br />Make sure these custom services take place the original ones.</p>
<pre class="language-csharp"><code>context.Services.AddTransient<DataSubmissionService, DataSubmissionService>()
.AddTransient<DataSubmissionService, CustomDataSubmissionService>();
context.Services.AddTransient<FormBusinessService, FormBusinessService>()
.AddTransient<FormBusinessService, CustomFormBusinessService>();</code></pre>
</li>
<li>Since we have changed the way FORM retrieves form elements, so we have to remove redundant elements and empty field sets before rendering the form. To do this, we need a custom form container block controller.
<pre class="language-csharp"><code>[TemplateDescriptor(AvailableWithoutTag = true, Default = true, ModelType = typeof(FormContainerBlock), TemplateTypeCategory = TemplateTypeCategories.MvcPartialController)]
public class CustomFormContainerBlockController: FormContainerBlockController, IRenderTemplate<FormContainerBlock>
{
public override ActionResult Index(FormContainerBlock currentBlock) // Note: convention, the parameter must be "currentBlock", not formBlock or another
{
var result = base.Index(currentBlock) as PartialViewResult;
var formModel = result.Model as FormContainerBlock;
if (formModel != null)
{
var steps = new List<IFormStep>(formModel.Form.Steps);
var allSubItems = new List<ContentReference>();
// find all sub items (child of FieldSetElement)
foreach (var step in steps)
{
foreach(var el in step.Elements)
{
if (el.SourceContent is IFieldSet)
{
var fieldSetElement = el.SourceContent as IFieldSet;
if (fieldSetElement.ElementsArea != null && fieldSetElement.ElementsArea.Items.Count > 0)
{
allSubItems.AddRange(fieldSetElement.ElementsArea.Items.Select(i => i.ContentLink));
}
}
}
}
// Remove redundant elements
foreach (var step in steps)
{
var elements = new List<IFormElement>(step.Elements);
foreach (var el in step.Elements)
{
if (el.SourceContent is IFieldSet)
{
var fieldSetElement = el.SourceContent as IFieldSet;
if (fieldSetElement.ElementsArea == null || fieldSetElement.ElementsArea.Items.Count == 0)
{
elements.Remove(el);
}
}
if (allSubItems.Contains(el.SourceContent.ContentLink))
{
elements.Remove(el);
}
}
step.Elements = elements;
}
formModel.Form.Steps = steps;
}
return result;
}</code></pre>
</li>
<li>Finally, add some style for the field set element.
<pre class="language-csharp"><code>.fieldsetBlock fieldset {
border: 1px groove red;
padding: 1em;
}
.fieldsetBlock fieldset legend {
width: auto;
margin: 0 5px;
padding: 0 5px;
}</code></pre>
</li>
</ol>
</body>
</html>