How to make my firebase dynamic link redirect to my website on desktop and to my instant app on mobile - firebase-dynamic-links

I have an instant app and a Firebase dynamic link which redirects to this instant app.
But when I click the dynamic link on a computer, the link leads to a non existant page of my website.
According to Google doc : https://firebase.google.com/docs/dynamic-links/android/create
When users open a Dynamic Link on a desktop web browser, they will load this URL (unless the ofl parameter is specified). If you don't have a web equivalent to the linked content, the URL doesn't need to point to a valid web resource. In this situation, you should set up a redirect from this URL to, for example, your home page.
So I created a redirection for my dynamic link which redirects
/share/** to /
And it works, when I click the link on a computer I land on the homepage of my website.
But my Dynamic links also leads on my homepage and do not open my instant app anymore.
So my question is : how to configure a redirection which redirects desktop users from /share/** to / without breaking my instant app ?

#Simon,
it is possible to achieve it without manual link construction, just use the same builder
private static final String OTHER_PLATFORM_LINK_KEY = "ofl";
public static Task<ShortDynamicLink> createShortDynamicLink(Uri deepLink, Uri imageUrl, String title, String description) {
DynamicLink dynamicLink = createDynamicLink(deepLink, imageUrl, title, description);
return FirebaseDynamicLinks.getInstance().createDynamicLink()
.setLongLink(dynamicLink.getUri())
.buildShortDynamicLink();
}
public static DynamicLink createDynamicLink(Uri deepLink, Uri imageUrl, String title, String description) {
DynamicLink dynamicLink = getDynamicLinkBuilder(deepLink, imageUrl, title, description)
.buildDynamicLink();
String longDynamicLink = String.valueOf(dynamicLink.getUri());
longDynamicLink += '&' + OTHER_PLATFORM_LINK_KEY + '=' + DomainConstants.OTHER_PLATFORM_LINK;
return FirebaseDynamicLinks.getInstance().createDynamicLink()
.setLongLink(Uri.parse(longDynamicLink))
.buildDynamicLink();
}
public static DynamicLink.Builder getDynamicLinkBuilder(Uri deepLink, Uri imageUrl, String title, String description) {
return FirebaseDynamicLinks.getInstance().createDynamicLink()
.setLink(deepLink)
.setDomainUriPrefix(DomainConstants.DYNAMIC_LINK_DOMAIN_URI_PREFIX)
.setAndroidParameters(new DynamicLink.AndroidParameters.Builder().build())
.setIosParameters(new DynamicLink.IosParameters.Builder(DomainConstants.IOS_BUNDLE_ID)
.setAppStoreId(DomainConstants.APP_STORE_ID)
.setMinimumVersion(DomainConstants.IOS_MINIMUM_VERSION)
.build())
.setSocialMetaTagParameters(
new DynamicLink.SocialMetaTagParameters.Builder()
.setTitle(title)
.setDescription(description)
.setImageUrl(imageUrl)
.build())
.setNavigationInfoParameters(new DynamicLink.NavigationInfoParameters.Builder()
.setForcedRedirectEnabled(true)
.build());
}

Add the "ofl" parameter to the url to make the FDL redirects another URL on desktop.
Unfortunately, it is not possible to add this parameter with the Android builder.
So you have to manually create the link, and use the "setLongLink" method as you can see here at the bottom of the page : https://firebase.google.com/docs/dynamic-links/android/create#shorten-a-long-dynamic-link
Task<ShortDynamicLink> shortLinkTask = FirebaseDynamicLinks.getInstance().createDynamicLink()
.setLongLink(Uri.parse("https://example.page.link/?link=https://www.example.com/&apn=com.example.android&ibn=com.example.ios"))
.buildShortDynamicLink()
.addOnCompleteListener(this, new OnCompleteListener<ShortDynamicLink>() {
#Override
public void onComplete(#NonNull Task<ShortDynamicLink> task) {
if (task.isSuccessful()) {
// Short link created
Uri shortLink = task.getResult().getShortLink();
Uri flowchartLink = task.getResult().getPreviewLink();
} else {
// Error
// ...
}
}
});
I created my own builder to include the ofl parameter. If it can helps :
public class DynamicLinkBuilder {
private String dynamicLink = "https://example.com/foo"
public DynamicLinkBuilder(String link) {
this.dynamicLink += "?link=" + link;
}
public DynamicLinkBuilder addMinVersion(int minVersion){
dynamicLink += "&amv=" + minVersion;
return this;
}
public DynamicLinkBuilder addIosUrl(String iosUrl){
dynamicLink += "&ifl=" + iosUrl;
return this;
}
public DynamicLinkBuilder addDesktopUrl(String desktopUrl){
dynamicLink += "&ofl=" + desktopUrl;
return this;
}
public DynamicLinkBuilder addFallbackUrl(String fallbackUrl){
dynamicLink += "&afl=" + fallbackUrl;
return this;
}
public DynamicLinkBuilder addPackageName(String packageName){
dynamicLink += "&apn=" + packageName;
return this;
}
public DynamicLinkBuilder addSocialMediaLogo(String logoUrl){
dynamicLink += "&si=" + logoUrl;
return this;
}
public DynamicLinkBuilder addSocialMediaTitle(String title){
dynamicLink += "&st=" + Uri.encode(title);
return this;
}
public DynamicLinkBuilder addSocialMediaDescription(String description){
dynamicLink += "&sd=" + Uri.encode(description);
return this;
}
public String build(){
return dynamicLink;
}
}

I solved this in a similar way but with shorter code:
As other have mentioned, you must manually add an 'ofl' parameter to the link. My method was:
// Grab link from Firebase builder
guard var longDynamicLink = shareLink.url else { return }
// Parse URL to string
var urlStr = longDynamicLink.absoluteString
// Append the ofl fallback (ofl param specifies a device other than ios or android)
urlStr = urlStr + "&ofl=https://www.meetmaro.com/"
// Convert back to a URL
var urlFinal = URL(string: urlStr)!
// Shorten the url & check for errors
DynamicLinkComponents.shortenURL(urlFinal, options: nil, completion:{ [weak self] url,warnings,error in
if let _ = error{
return
}
if let warnings = warnings{
for warning in warnings{
print("Shorten URL warnings: ", warning)
}
}
guard let shortUrl = url else {return}
// prompt the user with UIActivityViewController
self?.showShareSheet(url: shortUrl)
})
The final URL can then be used to present the shareable panel with another function like:
self.showShareSheet(url: finalUrl) which triggers the UIActivityViewController
Credit to http://ostack.cn/?qa=168161/ for the original idea
More about ofl: https://firebase.google.com/docs/dynamic-links/create-manually?authuser=3#general-params

Based on #Ivan Karpiuk answer:
Documentation:
ofl The link to open on platforms beside Android and iOS. This is useful to specify a different behavior on desktop, like displaying a full web page of the app content/payload (as specified by param link) with another dynamic link to install the app.
Add this DynamicLink.Builder extension:
private fun DynamicLink.Builder.otherPlatformParameters(): DynamicLink.Builder {
var longDynamicLink = this.buildDynamicLink().uri.toString()
longDynamicLink += "&ofl=" + YOUR_URL
longLink = Uri.parse(longDynamicLink)
return this
}
Replace YOUR_URL with yours and then:
FirebaseDynamicLinks.getInstance().createDynamicLink()
.setLink(...)
.setDomainUriPrefix(...)
.setAndroidParameters(...)
.setIosParameters(...)
.setSocialMetaTagParameters(...)
.otherPlatformParameters()
.buildShortDynamicLink()
So we create a long link via createDynamicLink(), add ofl with otherPlatformParameters() and then convert it to a short link buildShortDynamicLink()

Related

problem showing PDF in Blazor page from byte array

I have gone through all the suggestions for how to take a byte array stored in SQL Server db as varbinary and display it as PDF in a Blazor website. I'm successful in ASP.Net with the aspx pages and code behind but I can't seem to find the right combination for Blazor (ShowPDF.razor and code behind ShowPDF.razor.cs)
Here is what I have as variants in the code behind:
The aReport.ReportDocument is returning a byte array of aReport.DocumentSize from the DB
FileStreamResult GetPDF()
{
var pdfStream = new System.IO.MemoryStream();
this.rdb = new ReportData();
aReport = rdb.GetWithReport(1);
pdfStream.Write(aReport.ReportDocument, 0, aReport.DocumentSize);
pdfStream.Position = 0;
return new FileStreamResult(pdfStream, new Microsoft.Net.Http.Headers.MediaTypeHeaderValue("application/pdf"));
}
OR 2. direct binary array to base 64 encoding:
return Convert.ToBase64String(aReport.ReportDocument);
While those two processes return the data, I'm unable to find how to set up the razor page to show the result. I have tried:
<object src="#Url.Action("GetPDF")"/>
and other variants without success.
Thanks!
Ok, I finally found the resolution for this.
The ShowPDF.razor.cs code behind page is:
public partial class ShowPDF: ComponentBase
{
private IReportData rdb; // the database
private ReportModel aReport; // report model
/*
aReport.ReportDocument holds the byte[]
*/
string GetPDF(int ReportId)
{
this.rdb = new ReportData();
aReport = rdb.GetWithReport(ReportId);
return "data:application/pdf;base64," + Convert.ToBase64String(aReport.ReportDocument);
}
}
and the ShowPDF.razor page is:
#page "/ShowPDF"
#page "/ShowPDF/{Report:int}"
#code {
[Parameter]
public int Report { get; set; }
}
<embed src="#GetPDF(Report)" visible="false" width="1500" height="2000" />
I'm afraid this solution is not optimal for medium size or large PDF files. Nobody sets systematically image source as base64 string. It should be the same for PDF files. Browsers will appreciate downloading PDF in a separate thread not in the HTML code rendering.
In Blazor, this can be easy achieved using a custom middleware.
namespace MyMiddlewares
{
public class ShowPdf
{
public ShowPdf(RequestDelegate next)
{
//_next = next; no call to _next.Invoke(context) because the handler is at the end of the request pipeline, so there will be no next middleware to invoke.
}
public async Task Invoke(HttpContext context)
{
byte[] pdfBytes = getPdfFromDb(context.Request.Query["pdfid"]);
context.Response.ContentType = "application/pdf";
context.Response.Headers.Add("Content-Disposition",
"attachment; " +
"filename=\"mypdf.pdf\"; " +
"size=" + pdfBytes.Length + "; " +
"creation-date=" + DateTime.Now.ToString("R").Replace(",", "") + "; " +
"modification-date=" + DateTime.Now.ToString("R").Replace(",", "") + "; " +
"read-date=" + DateTime.Now.ToString("R").Replace(",", ""));
await context.Response.Body.WriteAsync(pdfBytes);
}
}
public static class ShowPdfExtensions
{
public static IApplicationBuilder UseShowPdf(this IApplicationBuilder builder)
{
return builder.UseMiddleware<ShowPdf>();
}
}
}
In the Configure method of Startup.cs, you add (before app.UseStaticFiles();)
app.MapWhen(
context => context.Request.Path.ToString().Contains("ShowPdf.mdwr", StringComparison.InvariantCultureIgnoreCase),
appBranch => {
appBranch.UseShowPdf();
});
So, this URL will download a PDF file: /ShowPdf.mdwr?pdfid=idOfMyPdf
If embedding is required, this URL may be used in a PDF viewer.

How do I use IViewLocationExtender with Razor Pages to render device specific pages

Currently we are building a web application, desktop first, that needs device specific Razor Pages for specific pages. Those pages are really different from their Desktop version and it makes no sense to use responsiveness here.
We have tried to implement our own IViewLocationExpander and also tried to use the MvcDeviceDetector library (which is basically doing the same). Detection of the device type is no problem but for some reason the device specific page is not picked up and it is constantly falling back to the default Index.cshtml.
(edit: We're thinking about implementing something based on IPageConvention, IPageApplicationModelProvider or something ... ;-))
Index.mobile.cshtml
Index.cshtml
We have added the following code using the example of MvcDeviceDetector:
public static IMvcBuilder AddDeviceDetection(this IMvcBuilder builder)
{
builder.Services.AddDeviceSwitcher<UrlSwitcher>(
o => { },
d => {
d.Format = DeviceLocationExpanderFormat.Suffix;
d.MobileCode = "mobile";
d.TabletCode = "tablet";
}
);
return builder;
}
and are adding some route mapping
routes.MapDeviceSwitcher();
We expected to see Index.mobile.cshtml to be picked up when selecting a Phone Emulation in Chrome but that didnt happen.
edit Note:
we're using a combination of Razor Views/MVC (older sections) and Razor Pages (newer sections).
also not every page will have a mobile implementation. That's what would have a IViewLocationExpander solution so great.
edit 2
I think the solution would be the same as how you'd implement Culture specific Razor Pages (which is also unknown to us ;-)). Basic MVC supports Index.en-US.cshtml
Final Solution Below
If this is a Razor Pages application (as opposed to an MVC application) I don't think that the IViewLocationExpander interface is much use to you. As far as I know, it only works for partials, not routeable pages (i.e. those with an #page directive).
What you can do instead is to use Middleware to determine whether the request comes from a mobile device, and then change the file to be executed to one that ends with .mobile. Here's a very rough and ready implementation:
public class MobileDetectionMiddleware
{
private readonly RequestDelegate _next;
public async Task Invoke(HttpContext context)
{
if(context.Request.IsFromAMobileDevice())
{
context.Request.Path = $"{context.Request.Path}.mobile";
}
await _next.Invoke(context);
}
}
It's up to you how you want to implement the IsFromAMobileDevice method to determine the nature of the user agent. There's nothing stopping you using a third party library that can do the check reliably for you. Also, you will probably only want to change the path under certain conditions - such as where there is a device specific version of the requested page.
Register this in your Configure method early:
app.UseMiddleware<MobileDetectionMiddleware>();
I've finally found the way to do it convention based. I have implemented a IViewLocationExpander in order to tackle the device handling for basic Razor Views (including Layouts) and I've implemented IPageRouteModelConvention + IActionConstraint to handle devices for Razor Pages.
Note: this solution only seems to be working on ASP.NET Core 2.2 and up though. For some reason 2.1.x and below is clearing the constraints (tested with a breakpoint in a destructor) after they've been added (can probably be fixed).
Now I can have /Index.mobile.cshtml /Index.desktop.cshtml etc. in both MVC and Razor Pages.
Note: This solution can also be used to implement a language/culture specific Razor Pages (eg. /Index.en-US.cshtml /Index.nl-NL.cshtml)
public class PageDeviceConvention : IPageRouteModelConvention
{
private readonly IDeviceResolver _deviceResolver;
public PageDeviceConvention(IDeviceResolver deviceResolver)
{
_deviceResolver = deviceResolver;
}
public void Apply(PageRouteModel model)
{
var path = model.ViewEnginePath; // contains /Index.mobile
var lastSeparator = path.LastIndexOf('/');
var lastDot = path.LastIndexOf('.', path.Length - 1, path.Length - lastSeparator);
if (lastDot != -1)
{
var name = path.Substring(lastDot + 1);
if (Enum.TryParse<DeviceType>(name, true, out var deviceType))
{
var constraint = new DeviceConstraint(deviceType, _deviceResolver);
for (var i = model.Selectors.Count - 1; i >= 0; --i)
{
var selector = model.Selectors[i];
selector.ActionConstraints.Add(constraint);
var template = selector.AttributeRouteModel.Template;
var tplLastSeparator = template.LastIndexOf('/');
var tplLastDot = template.LastIndexOf('.', template.Length - 1, template.Length - Math.Max(tplLastSeparator, 0));
template = template.Substring(0, tplLastDot); // eg Index.mobile -> Index
selector.AttributeRouteModel.Template = template;
var fileName = template.Substring(tplLastSeparator + 1);
if ("Index".Equals(fileName, StringComparison.OrdinalIgnoreCase))
{
selector.AttributeRouteModel.SuppressLinkGeneration = true;
template = selector.AttributeRouteModel.Template.Substring(0, Math.Max(tplLastSeparator, 0));
model.Selectors.Add(new SelectorModel(selector) { AttributeRouteModel = { Template = template } });
}
}
}
}
}
protected class DeviceConstraint : IActionConstraint
{
private readonly DeviceType _deviceType;
private readonly IDeviceResolver _deviceResolver;
public DeviceConstraint(DeviceType deviceType, IDeviceResolver deviceResolver)
{
_deviceType = deviceType;
_deviceResolver = deviceResolver;
}
public int Order => 0;
public bool Accept(ActionConstraintContext context)
{
return _deviceResolver.GetDeviceType() == _deviceType;
}
}
}
public class DeviceViewLocationExpander : IViewLocationExpander
{
private readonly IDeviceResolver _deviceResolver;
private const string ValueKey = "DeviceType";
public DeviceViewLocationExpander(IDeviceResolver deviceResolver)
{
_deviceResolver = deviceResolver;
}
public void PopulateValues(ViewLocationExpanderContext context)
{
var deviceType = _deviceResolver.GetDeviceType();
if (deviceType != DeviceType.Other)
context.Values[ValueKey] = deviceType.ToString();
}
public IEnumerable<string> ExpandViewLocations(ViewLocationExpanderContext context, IEnumerable<string> viewLocations)
{
var deviceType = context.Values[ValueKey];
if (!string.IsNullOrEmpty(deviceType))
{
return ExpandHierarchy();
}
return viewLocations;
IEnumerable<string> ExpandHierarchy()
{
var replacement = $"{{0}}.{deviceType}";
foreach (var location in viewLocations)
{
if (location.Contains("{0}"))
yield return location.Replace("{0}", replacement);
yield return location;
}
}
}
}
public interface IDeviceResolver
{
DeviceType GetDeviceType();
}
public class DefaultDeviceResolver : IDeviceResolver
{
public DeviceType GetDeviceType() => DeviceType.Mobile;
}
public enum DeviceType
{
Other,
Mobile,
Tablet,
Normal
}
Startup
services.AddMvc(o => { })
.SetCompatibilityVersion(CompatibilityVersion.Version_2_2)
.AddRazorOptions(o =>
{
o.ViewLocationExpanders.Add(new DeviceViewLocationExpander(new DefaultDeviceResolver()));
})
.AddRazorPagesOptions(o =>
{
o.Conventions.Add(new PageDeviceConvention(new DefaultDeviceResolver()));
});

Razor page routing based on different domains

I'm trying to setup a single ASP.NET Core Razor Web app localized for use on multi domains. I have the localization working, with one different language for each domain. But right now I want to have the .com domain accepting a routing parameter, to make the URL path decide with language to show.
Something like:
www.mysite.pt - no custom routing - www.mysite.pt/PageA works, localized in Portuguese.
www.mysite.com - custom routing - www.mysite.com/us/PageA goes to PageA, localized in en-US. But www.mysite.com/PageA should return a 404, as for this domain every page needs the country parameter.
For MVC this could be achieved by using the MapRoute with a custom IRouteConstraint to filter by domain.
However with Razor pages, I only see the option to go with the conventions and add a class derived from IPageRouteModelConvention.
But I don't see a way on the IPageRouteModelConvention methodology to use a IRouteConstraint.
Is there a way to do this?
Not exactly the best solution... but worked this out:
On ConfigureServices added a custom convention that takes a country parameter only with two country codes US and CA:
options.Conventions.Add(new CountryTemplateRouteModelConvention());
wethe this class being:
public class CountryTemplateRouteModelConvention : IPageRouteModelConvention
{
public void Apply(PageRouteModel model)
{
var selectorCount = model.Selectors.Count;
for (var i = 0; i < selectorCount; i++)
{
var selector = model.Selectors[i];
// selector.AttributeRouteModel.SuppressLinkGeneration = false;
//we are not adding the selector, but replacing the existing one
model.Selectors.Add(new SelectorModel
{
AttributeRouteModel = new AttributeRouteModel
{
Order = -1,
Template = AttributeRouteModel.CombineTemplates(#"{country:length(2):regex(^(us|ca)$)}", selector.AttributeRouteModel.Template),
}
});
}
}
}
Then, before the UseMvc on Configure, I used two types of Rewrite rules:
var options = new RewriteOptions();
options.Add(new CountryBasedOnDomainRewriteRule(domains: GetDomainsWhereCountryComesFromDomain(Configuration)));
options.Add(new CountryBasedOnPathRewriteRule(domains: GetDomainsWhereCountryComesFromPath(Configuration)));
app.UseRewriter(options);
The methods GetDomainsWhereCountryComesFromDomain and GetDomainsWhereCountryComesFromPath just read from the appsettings the domains where I want to have a single language, and the domains where I want the language to be obtained from the URL path.
Now, the two IRule classes:
public class CountryBasedOnPathRewriteRule : IRule
{
private readonly string[] domains;
public CountryBasedOnPathRewriteRule(string[] domains)
{
this.domains = domains;
}
public void ApplyRule(RewriteContext context)
{
string hostname = context.HttpContext.Request.Host.Host.ToLower();
if (!domains.Contains(hostname)) return;
//only traffic that has the country on the path is valid. examples:
// www.mysite.com/ -> www.mysite.com/US/
// www.mysite.com/Cart -> www.mysite.com/US/Cart
var path = context.HttpContext.Request.Path.ToString().ToLower();
/* let's exclude the error page, as method UseExceptionHandler doesn't accept the country parameter */
if (path == "/" || path == "/error")
{
//redirect to language default
var response = context.HttpContext.Response;
response.StatusCode = (int)HttpStatusCode.Moved;
response.Headers[HeaderNames.Location] = "/us/"; //default language/country
context.Result = RuleResult.EndResponse;
}
string pathFirst = path.Split('/')?[1];
if (pathFirst.Length != 2) /* US and CA country parameter is already enforced by the routing */
{
var response = context.HttpContext.Response;
response.StatusCode = (int)HttpStatusCode.NotFound;
context.Result = RuleResult.EndResponse;
}
}
}
public class CountryBasedOnDomainRewriteRule : IRule
{
private readonly string[] domains;
public CountryBasedOnDomainRewriteRule(string[] domains)
{
this.domains = domains;
}
public void ApplyRule(RewriteContext context)
{
string hostname = context.HttpContext.Request.Host.Host.ToLower();
if (!domains.Contains(hostname)) return;
var path = context.HttpContext.Request.Path.ToString().ToLower();
string pathFirst = path.Split('/')?[1];
if (pathFirst.Length == 2) //we are trying to use www.mysite.co.uk/us which is not allowed
{
var response = context.HttpContext.Response;
response.StatusCode = (int)HttpStatusCode.NotFound;
context.Result = RuleResult.EndResponse;
}
}
}
And that's it.

Trying to use PlaceRequest the right way

i have two Presenters: A DevicePresenter and a ContainerPresenter. I place a PlaceRequest in the DevicePresenter to call the ContainerPresenter with some parameters like this:
PlaceRequest request = new PlaceRequest.Builder()
.nameToken("containersPage")
.with("action","editContainer")
.with("containerEditId", selectedContainerDto.getUuid().toString())
.build();
placeManager.revealPlace(request);
In my ContainersPresenter i have this overridden method:
#Override
public void prepareFromRequest(PlaceRequest placeRequest) {
Log.debug("prepareFromRequest in ContainersPresenter");
super.prepareFromRequest(placeRequest);
String actionString = placeRequest.getParameter("action", "");
String id;
//TODO: Should we change that to really retrieve the object from the server? Or should we introduce a model that keeps all values and inject that into all presenters?
if (actionString.equals("editContainer")) {
try {
id = placeRequest.getParameter("id", null);
for(ContainerDto cont : containerList) {
Log.debug("Compare " + id + " with " + cont.getUuid());
if(id.equals(cont.getUuid())) {
containerDialog.setCurrentContainerDTO(new ContainerDto());
addToPopupSlot(containerDialog);
break;
}
}
} catch (NumberFormatException e) {
Log.debug("id cannot be retrieved from URL");
}
}
}
But when revealPlace is called, the URL in the browser stays the same and the default presenter (Home) is shown instead.
When i print the request, it seems to be fine:
PlaceRequest(nameToken=containersPage, params={action=editContainer, containerEditId=8fa5f730-fe0f-11e3-a3ac-0800200c9a66})
And my NameTokens are like this:
public class NameTokens {
public static final String homePage = "!homePage";
public static final String containersPage = "!containersPage";
public static final String devicesPage = "!devicesPage";
public static String getHomePage() {
return homePage;
}
public static String getDevicesPage() {
return devicesPage;
}
public static String getContainersPage() {
return containersPage;
}
}
What did i miss? Thanks!
In your original code, when constructing your PlaceRequest, you forgot the '!' at the beginning of your nametoken.
.nameToken("containersPage")
while your NameTokens entry is
public static final String containersPage = "!containersPage";
As you noted, referencing the constant in NameTokens is less prone to such easy mistakes to make!
Sometimes the problem exists "between the ears". If i avoid strings but use the proper symbol from NameTokens like
PlaceRequest request = new PlaceRequest.Builder()
.nameToken(NameTokens.containersPage)
.with("action","editContainer")
.with("containerEditId", selectedContainerDto.getUuid().toString())
.build();
it works just fine. Sorry!

Can I use RE-Captcha with Wicket?

Can I use recaptcha with apache wicket 1.5.3? Is there some good example?
In terms of Google reCAPTCHA v2, you can just follow its instruction, which is straightforward.
First of all, go to Google reCAPTCHA, and register your application there. Then you can work on the client and server sides respectively as below:
On the client side (see ref)
First, paste the snippet below <script...></script> before the closing tag on your HTML template, for example:
<script src='https://www.google.com/recaptcha/api.js'></script>
</head>
Then paste the snippet below <div...></div> at the end of the where you want the reCAPTCHA widget to appear, for example:
<div class="g-recaptcha" data-sitekey="{your public site key given by Google reCAPTCHA}"></div>
</form>
That's all on the client side.
On the server side (see ref)
When a user submits the form, you need to get the user response token from the g-recaptcha-response POST parameter. Then use the token, together with the secret key given by Google reCAPTCHA, and optional with the user's IP address, and then POST a request to the Google reCAPTCHA API. You'll then get the response from Google reCAPTHA, indicating whether the form verification succeeds or fails.
Below is the sample code on the server side.
User summits a Wicket form (Wicket 6 in this example):
protected void onSubmit() {
HttpServletRequest httpServletRequest = (HttpServletRequest)getRequest().getContainerRequest();
boolean isValidRecaptcha = ReCaptchaV2.getInstance().verify(httpServletRequest);
if(!isValidRecaptcha){
verificationFailedFeedbackPanel.setVisible(true);
return;
}
// reCAPTCHA verification succeeded, carry on handling form submission
...
}
ReCaptchaV2.java (Just Java, web framework independent)
import javax.servlet.http.HttpServletRequest;
import org.apache.log4j.Logger;
import org.codehaus.jackson.map.ObjectMapper;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
import org.springframework.web.client.RestTemplate;
public class ReCaptchaV2 {
private final static Logger logger = Logger.getLogger(ReCaptchaV2.class);
private final static String VERIFICATION_URL = "https://www.google.com/recaptcha/api/siteverify";
private final static String SECRET = "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx";
private static ReCaptchaV2 instance = new ReCaptchaV2();
private ReCaptchaV2() {}
public static ReCaptchaV2 getInstance() {
return instance;
}
private boolean verify(String recaptchaUserResponse, String remoteip) {
boolean ret = false;
if (recaptchaUserResponse == null) {
return ret;
}
RestTemplate rt = new RestTemplate();
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED);
MultiValueMap<String, String> map= new LinkedMultiValueMap<String, String>();
map.add("secret", SECRET);
map.add("response", recaptchaUserResponse);
if (remoteip != null) {
map.add("remoteip", remoteip);
}
HttpEntity<MultiValueMap<String, String>> httpEntity = new HttpEntity<MultiValueMap<String, String>>(map, headers);
ResponseEntity<String> res = null;
try {
res = rt.exchange(VERIFICATION_URL, HttpMethod.POST, httpEntity, String.class);
} catch (Exception e) {
logger.error("Exception: " + e.getMessage());
}
if (res == null || res.getBody() == null) {
return ret;
}
Response response = null;
try {
response = new ObjectMapper().readValue(res.getBody(), Response.class);
} catch (Exception e) {
logger.error("Exception: " + e.getMessage());
}
if (response != null && response.isSuccess()) {
ret = true;
}
logger.info("Verification result: " + ret);
return ret;
}
public boolean verify(HttpServletRequest httpServletRequest) {
boolean ret = false;
if (httpServletRequest == null) {
return ret;
}
String recaptchaUserResponse = httpServletRequest.getParameter("g-recaptcha-response");
String remoteAddr = httpServletRequest.getRemoteAddr();
return verify(recaptchaUserResponse, remoteAddr);
}
}
Response.java (Java POJO)
public class Response {
private String challenge_ts;
private String hostname;
private boolean success;
public Response() {}
public String getChallenge_ts() {
return challenge_ts;
}
public void setChallenge_ts(String challenge_ts) {
this.challenge_ts = challenge_ts;
}
public String getHostname() {
return hostname;
}
public void setHostname(String hostname) {
this.hostname = hostname;
}
public boolean isSuccess() {
return success;
}
public void setSuccess(boolean success) {
this.success = success;
}
#Override
public String toString() {
return "ClassPojo [challenge_ts = " + challenge_ts + ", hostname = " + hostname + ", success = " + success + "]";
}
}
Have you read this?
I have added the guide here in case page disappears.
Usage
We will create a panel called RecaptchaPanel. In order to use this component to your application all you'll have to do is this:
add(new RecaptchaPanel("recaptcha"));
and of course, add the component in your markup:
<div wicket:id="recaptcha"></div>
Implementation
Implementation is simple. All you have to do, is to follow several steps:
Add recaptcha dependency to your project
<dependency>
<groupid>net.tanesha.recaptcha4j</groupid>
<artifactid>recaptcha4j</artifactid>
<version>0.0.7</version>
</dependency>
This library hides the implementation details and expose an API for dealing with recaptcha service.
Create associated markup (RecaptchaPanel.html)
<wicket:panel><div wicket:id="captcha"></div></wicket:panel>
Create RecaptchaPanel.java
import net.tanesha.recaptcha.ReCaptcha;
import net.tanesha.recaptcha.ReCaptchaFactory;
import net.tanesha.recaptcha.ReCaptchaImpl;
import net.tanesha.recaptcha.ReCaptchaResponse;
/**
* Displays recaptcha widget. It is configured using a pair of public/private keys which can be registered at the
* following location:
*
* https://www.google.com/recaptcha/admin/create
* <br>
* More details about recaptcha API: http://code.google.com/apis/recaptcha/intro.html
*
* #author Alex Objelean
*/
#SuppressWarnings("serial")
public class RecaptchaPanel extends Panel {
private static final Logger LOG = LoggerFactory.getLogger(RecaptchaPanel.class);
#SpringBean
private ServiceProvider serviceProvider;
public RecaptchaPanel(final String id) {
super(id);
final ReCaptcha recaptcha = ReCaptchaFactory.newReCaptcha(serviceProvider.getSettings().getRecaptchaPublicKey(),
serviceProvider.getSettings().getRecaptchaPrivateKey(), false);
add(new FormComponent<void>("captcha") {
#Override
protected void onComponentTagBody(final MarkupStream markupStream, final ComponentTag openTag) {
replaceComponentTagBody(markupStream, openTag, recaptcha.createRecaptchaHtml(null, null));
}
#Override
public void validate() {
final WebRequest request = (WebRequest)RequestCycle.get().getRequest();
final String remoteAddr = request.getHttpServletRequest().getRemoteAddr();
final ReCaptchaImpl reCaptcha = new ReCaptchaImpl();
reCaptcha.setPrivateKey(serviceProvider.getSettings().getRecaptchaPrivateKey());
final String challenge = request.getParameter("recaptcha_challenge_field");
final String uresponse = request.getParameter("recaptcha_response_field");
final ReCaptchaResponse reCaptchaResponse = reCaptcha.checkAnswer(remoteAddr, challenge, uresponse);
if (!reCaptchaResponse.isValid()) {
LOG.debug("wrong captcha");
error("Invalid captcha!");
}
}
});
}
}
</void>
Things to notice:
ServiceProvider - is a spring bean containing reCaptcha configurations (public key and private key). These keys are different depending on the domain where your application is deployed (by default works for any key when using localhost domain). You can generate keys here: https://www.google.com/recaptcha/admin/create
The RecaptchaPanel contains a FormComponent, which allows implementing validate method, containing the validation logic.
Because reCaptcha use hardcoded values for hidden fields, this component cannot have multiple independent instances on the same page.
Maybe the xaloon wicket components can be a solution for you. They have a Recaptcha plugin.