Skip to content
This repository has been archived by the owner on Jun 9, 2021. It is now read-only.

Commit

Permalink
Add interactive forms to buttons
Browse files Browse the repository at this point in the history
This change adds the ability to specify a JSON-based form for a given
button, which will get automatically rendered when the button is pressed.
The submitted data is available as serialized JSON in the ${BUTTON_FORM_DATA}
variable.

For the specification of what a form looks like and it's serialized result,
look at README.md in the change.
  • Loading branch information
Itay Neeman committed Dec 24, 2016
1 parent 5644887 commit 2a5c793
Show file tree
Hide file tree
Showing 22 changed files with 454 additions and 88 deletions.
65 changes: 65 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ The Pull Request Notifier for Bitbucket can:
* Can invoke CSRF protected systems, using the ${INJECTION_URL_VALUE} variable. How to to that with Jenkins is described below.
* Be configured to only trigger if the pull request mathches a filter. A filter text is constructed with any combination of the variables and then a regexp is constructed to match that text.
* Add buttons to pull request view in Bitbucket. And map those buttons to URL invocations. This can be done by setting the filter string to ${BUTTON_TRIGGER_TITLE} and the filter regexp to title of button.
* Buttons can have forms associated with them, and then submit the form data using the ${BUTTON_FORM_DATA} variable.
* Authenticate with HTTP basic authentication.
* Optionally allow any SSL certificate.
* Use custom SSL key store, type and password.
Expand Down Expand Up @@ -56,6 +57,7 @@ The filter text as well as the URL support variables. These are:
* ${PULL_REQUEST_ACTION} Example: OPENED
* ${PULL_REQUEST_STATE} Example: DECLINED, MERGED, OPEN
* ${BUTTON_TRIGGER_TITLE} Example: Trigger Notification
* ${BUTTON_FORM_DATA} The form data that was submitted
* ${INJECTION_URL_VALUE} Value retrieved from any URL
* ${PULL_REQUEST_URL} Example: http://localhost:7990/projects/PROJECT_1/repos/rep_1/pull-requests/1
* ${PULL_REQUEST_USER_DISPLAY_NAME} Example: Some User
Expand Down Expand Up @@ -101,6 +103,69 @@ The ${PULL_REQUEST_USER...} contains information about the user who issued the e

You may want to use [Violation Comments to Stash plugin](https://wiki.jenkins-ci.org/display/JENKINS/Violation+Comments+to+Stash+Plugin) and/or [StashNotifier plugin](https://wiki.jenkins-ci.org/display/JENKINS/StashNotifier+Plugin) to report results back to Bitbucket.

#### Button Forms

For each button you can specify a form that will show up when the button is pressed. That form data will then be submitted and will be available in the ${BUTTON_FORM_DATA} variable. Additionally, the form itself can reference other variables (with the exception of the ${BUTTON_...} ones) and will have those resolved prior to rendering.

A form is defined as a JSON array. Here is an example that shows all possibilities:

```
[
{ "name": "var1",
"label": "var1 label",
"defaultValue": "you can put a variable like this: ${PULL_REQUEST_AUTHOR_NAME}",
"type": "input",
"required": false,
"description": "var1 description"
},
{ "name": "var2",
"label": "var2 label",
"defaultValue": "any string can go here",
"type": "textarea",
"required": false,
"description": "var2 description"
},
{ "name": "var3",
"label": "var3 label",
"defaultValue": "option2_name",
"options": [
{"label": "option1 label", "name": "option1_name"},
{"label": "option2 label", "name": "option2_name"},
{"label": "option3 label", "name": "option3_name"}
],
"type": "radio",
"required": true,
"description": "var3 description"
},
{ "name": "var4",
"label": "var4 label",
"type": "checkbox",
"required": true,
"options": [
{"label": "option1 label", "name": "option1_name", "defaultValue": true},
{"label": "option2 label", "name": "option2_name", "defaultValue": true}
],
"description": "var4 description"
}
]
```

You can see a screenshot [here](https://raw.githubusercontent.com/tomasbjerre/pull-request-notifier-for-bitbucket/master/sandbox/rendered_form.png) when rendered.

When submitted with the default values, it will look like this:

```
{
"var1":"you can put a variable like this: admin",
"var2":"any string can go here",
"var3":"option2_name",
"var4":[
"option1_name",
"option2_name"
]
}
```

### REST
Some rest resources are available. You can figure out the JSON structure by looking at the [DTO:s](https://github.com/tomasbjerre/pull-request-notifier-for-bitbucket/tree/master/src/main/java/se/bjurr/prnfb/presentation/dto).

Expand Down
20 changes: 18 additions & 2 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -197,6 +197,21 @@ Changelog of Pull Request Notifier for Bitbucket.
<version>${amps.version}</version>
<extensions>true</extensions>
<configuration>
<enableFastdev>false</enableFastdev>
<enableDevToolbox>false</enableDevToolbox>
<enablePde>false</enablePde>
<skipRestDocGeneration>true</skipRestDocGeneration>
<skipManifestValidation>true</skipManifestValidation>
<extractDependencies>false</extractDependencies>
<skipManifestValidation>true</skipManifestValidation>
<enableQuickReload>true</enableQuickReload>
<pluginArtifacts>
<pluginArtifact>
<groupId>com.atlassian.labs.plugins</groupId>
<artifactId>quickreload</artifactId>
<version>${quick.reload.version}</version>
</pluginArtifact>
</pluginArtifacts>
<products>
<product>
<id>bitbucket</id>
Expand Down Expand Up @@ -258,8 +273,9 @@ Changelog of Pull Request Notifier for Bitbucket.
</profiles>

<properties>
<bitbucket.version>4.8.1</bitbucket.version>
<bitbucket.version>4.11.1</bitbucket.version>
<bitbucket.data.version>${bitbucket.version}</bitbucket.data.version>
<quick.reload.version>2.0.0</quick.reload.version>
<amps.version>6.1.0</amps.version>
</properties>
</project>
</project>
Binary file added sandbox/rendered_form.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Original file line number Diff line number Diff line change
Expand Up @@ -145,7 +145,7 @@ public boolean isNotificationTriggeredByAction(PrnfbNotification notification,

if (notification.getFilterRegexp().isPresent() && notification.getFilterString().isPresent()
&& !compile(notification.getFilterRegexp().get())
.matcher(renderer.render(notification.getFilterString().get(), FALSE, clientKeyStore, shouldAcceptAnyCertificate))
.matcher(renderer.render(notification.getFilterString().get(), FALSE, FALSE, clientKeyStore, shouldAcceptAnyCertificate))
.find()) {
return FALSE;
}
Expand Down Expand Up @@ -176,9 +176,9 @@ public NotificationResponse notify(final PrnfbNotification notification, PrnfbPu
Optional<String> postContent = absent();
if (notification.getPostContent().isPresent()) {
postContent = of(
renderer.render(notification.getPostContent().get(), FALSE, clientKeyStore, shouldAcceptAnyCertificate));
renderer.render(notification.getPostContent().get(), FALSE, FALSE, clientKeyStore, shouldAcceptAnyCertificate));
}
String renderedUrl = renderer.render(notification.getUrl(), TRUE, clientKeyStore, shouldAcceptAnyCertificate);
String renderedUrl = renderer.render(notification.getUrl(), TRUE, FALSE, clientKeyStore, shouldAcceptAnyCertificate);
LOG.info(notification.getName() + " > " //
+ pullRequest.getFromRef().getId() + "(" + pullRequest.getFromRef().getLatestCommit() + ") -> " //
+ pullRequest.getToRef().getId() + "(" + pullRequest.getToRef().getLatestCommit() + ")" + " " //
Expand All @@ -192,7 +192,7 @@ public NotificationResponse notify(final PrnfbNotification notification, PrnfbPu
for (PrnfbHeader header : notification.getHeaders()) {
urlInvoker//
.withHeader(header.getName(),
renderer.render(header.getValue(), FALSE, clientKeyStore, shouldAcceptAnyCertificate));
renderer.render(header.getValue(), FALSE, FALSE, clientKeyStore, shouldAcceptAnyCertificate));
}
HttpResponse httpResponse = createInvoker().invoke(urlInvoker//
.withProxyServer(notification.getProxyServer()) //
Expand Down
29 changes: 25 additions & 4 deletions src/main/java/se/bjurr/prnfb/presentation/ButtonServlet.java
Original file line number Diff line number Diff line change
Expand Up @@ -14,13 +14,15 @@
import java.util.List;
import java.util.UUID;

import javax.servlet.http.HttpServletRequest;
import javax.ws.rs.Consumes;
import javax.ws.rs.DELETE;
import javax.ws.rs.GET;
import javax.ws.rs.POST;
import javax.ws.rs.Path;
import javax.ws.rs.PathParam;
import javax.ws.rs.Produces;
import javax.ws.rs.core.Context;
import javax.ws.rs.core.Response;

import se.bjurr.prnfb.http.NotificationResponse;
Expand All @@ -32,13 +34,15 @@
import se.bjurr.prnfb.settings.PrnfbButton;

import com.atlassian.annotations.security.XsrfProtectionExcluded;
import com.google.gson.Gson;

@Path("/settings/buttons")
public class ButtonServlet {
private final ButtonsService buttonsService;
private final SettingsService settingsService;
private final UserCheckService userCheckService;

private static final Gson gson = new Gson();

public ButtonServlet(ButtonsService buttonsService, SettingsService settingsService,
UserCheckService userCheckService) {
this.buttonsService = buttonsService;
Expand All @@ -57,6 +61,15 @@ public Response create(ButtonDTO buttonDto) {
return status(UNAUTHORIZED)//
.build();
}

if (buttonDto.getButtonForm() != null && !buttonDto.getButtonForm().isEmpty()) {
try {
gson.fromJson(buttonDto.getButtonForm(), Object.class);
} catch(com.google.gson.JsonSyntaxException ex) {
throw new Error("The form specification for the button must be a valid JSON string");
}
}

PrnfbButton prnfbButton = toPrnfbButton(buttonDto);
PrnfbButton created = this.settingsService.addOrUpdateButton(prnfbButton);
ButtonDTO createdDto = toButtonDto(created);
Expand Down Expand Up @@ -103,6 +116,13 @@ public Response get(@PathParam("repositoryId") Integer repositoryId, @PathParam(
Iterable<PrnfbButton> allowedButtons = this.userCheckService.filterAllowed(buttons);
List<ButtonDTO> dtos = toButtonDtoList(allowedButtons);
Collections.sort(dtos);

for(ButtonDTO dto : dtos) {
if (dto.getButtonForm() != null) {
dto.setButtonForm(this.buttonsService.getRenderedButtonFormData(repositoryId, pullRequestId, dto.getUuid(), dto.getButtonForm()));
}
}

return ok(dtos, APPLICATION_JSON).build();
}

Expand Down Expand Up @@ -150,16 +170,17 @@ public Response get(@PathParam("uuid") UUID uuid) {
@Path("{uuid}/press/repository/{repositoryId}/pullrequest/{pullRequestId}")
@XsrfProtectionExcluded
@Produces(APPLICATION_JSON)
public Response press(@PathParam("repositoryId") Integer repositoryId, @PathParam("pullRequestId") Long pullRequestId,
public Response press(@Context HttpServletRequest request, @PathParam("repositoryId") Integer repositoryId, @PathParam("pullRequestId") Long pullRequestId,
@PathParam("uuid") final UUID buttionUuid) {
String formData = request.getParameter("form");
PrnfbButton button = this.settingsService.getButton(buttionUuid);
if (!this.userCheckService.isAllowedUseButton(button)) {
return status(UNAUTHORIZED).build();
}
List<NotificationResponse> results = this.buttonsService.handlePressed(repositoryId, pullRequestId, buttionUuid);
List<NotificationResponse> results = this.buttonsService.handlePressed(repositoryId, pullRequestId, buttionUuid, formData);

ButtonPressDTO dto = toTriggerResultDto(button, results);
return ok(dto, APPLICATION_JSON).build();
}

}
}
19 changes: 18 additions & 1 deletion src/main/java/se/bjurr/prnfb/presentation/dto/ButtonDTO.java
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ public class ButtonDTO implements Comparable<ButtonDTO> {
private String repositorySlug;
private USER_LEVEL userLevel;
private UUID uuid;
private String buttonForm;

@Override
public int compareTo(ButtonDTO o) {
Expand Down Expand Up @@ -70,6 +71,13 @@ public boolean equals(Object obj) {
} else if (!this.confirmation.equals(other.confirmation)) {
return false;
}
if (this.buttonForm == null) {
if (other.buttonForm != null) {
return false;
}
} else if (!this.buttonForm.equals(other.buttonForm)) {
return false;
}
if (this.uuid == null) {
if (other.uuid != null) {
return false;
Expand All @@ -88,6 +96,10 @@ public String getName() {
return this.name;
}

public String getButtonForm() {
return this.buttonForm;
}

public Optional<String> getProjectKey() {
return Optional.fromNullable(this.projectKey);
}
Expand Down Expand Up @@ -118,6 +130,7 @@ public int hashCode() {
result = prime * result + ((this.userLevel == null) ? 0 : this.userLevel.hashCode());
result = prime * result + ((this.uuid == null) ? 0 : this.uuid.hashCode());
result = prime * result + ((this.confirmation == null) ? 0 : this.confirmation.hashCode());
result = prime * result + ((this.buttonForm == null) ? 0 : this.buttonForm.hashCode());
return result;
}

Expand All @@ -129,6 +142,10 @@ public void setName(String name) {
this.name = name;
}

public void setButtonForm(String buttonForm) {
this.buttonForm = buttonForm;
}

public void setProjectKey(String projectKey) {
this.projectKey = projectKey;
}
Expand All @@ -148,7 +165,7 @@ public void setUuid(UUID uuid) {
@Override
public String toString() {
return "ButtonDTO [name=" + this.name + ", userLevel=" + this.userLevel + ", uuid=" + this.uuid + ", repositorySlug="
+ this.repositorySlug + ", projectKey=" + this.projectKey + ", confirmation=" + this.confirmation + "]";
+ this.repositorySlug + ", projectKey=" + this.projectKey + ", buttonForm=" + this.buttonForm + ", confirmation=" + this.confirmation + "]";
}

}
29 changes: 22 additions & 7 deletions src/main/java/se/bjurr/prnfb/service/ButtonsService.java
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import static java.lang.Boolean.TRUE;
import static se.bjurr.prnfb.listener.PrnfbPullRequestAction.BUTTON_TRIGGER;
import static se.bjurr.prnfb.service.PrnfbVariable.BUTTON_TRIGGER_TITLE;
import static se.bjurr.prnfb.service.PrnfbVariable.BUTTON_FORM_DATA;

import java.util.HashMap;
import java.util.List;
Expand Down Expand Up @@ -54,12 +55,25 @@ public List<PrnfbButton> getButtons(Integer repositoryId, Long pullRequestId) {
return doGetButtons(notifications, clientKeyStore, pullRequest, shouldAcceptAnyCertificate);
}

public List<NotificationResponse> handlePressed(Integer repositoryId, Long pullRequestId, UUID buttonUuid) {
public String getRenderedButtonFormData(Integer repositoryId, Long pullRequestId, UUID buttonUuid, String formData) {
final PrnfbSettingsData settings = this.settingsService.getPrnfbSettingsData();
ClientKeyStore clientKeyStore = new ClientKeyStore(settings);
final PullRequest pullRequest = this.pullRequestService.getById(repositoryId, pullRequestId);
boolean shouldAcceptAnyCertificate = settings.isShouldAcceptAnyCertificate();

Map<PrnfbVariable, Supplier<String>> variables = getVariables(buttonUuid, formData);
PrnfbPullRequestAction pullRequestAction = BUTTON_TRIGGER;

PrnfbRenderer renderer = this.prnfbRendererFactory.create(pullRequest, pullRequestAction, null, variables);
return renderer.render(formData, false, true, clientKeyStore, shouldAcceptAnyCertificate);
}

public List<NotificationResponse> handlePressed(Integer repositoryId, Long pullRequestId, UUID buttonUuid, String formData) {
final PrnfbSettingsData prnfbSettingsData = this.settingsService.getPrnfbSettingsData();
ClientKeyStore clientKeyStore = new ClientKeyStore(prnfbSettingsData);
boolean shouldAcceptAnyCertificate = prnfbSettingsData.isShouldAcceptAnyCertificate();
final PullRequest pullRequest = this.pullRequestService.getById(repositoryId, pullRequestId);
return doHandlePressed(buttonUuid, clientKeyStore, shouldAcceptAnyCertificate, pullRequest);
return doHandlePressed(buttonUuid, clientKeyStore, shouldAcceptAnyCertificate, pullRequest, formData);
}

private boolean isTriggeredByAction(ClientKeyStore clientKeyStore, List<PrnfbNotification> notifications,
Expand Down Expand Up @@ -90,7 +104,7 @@ List<PrnfbButton> doGetButtons(List<PrnfbNotification> notifications, ClientKeyS
final PullRequest pullRequest, boolean shouldAcceptAnyCertificate) {
List<PrnfbButton> allFoundButtons = newArrayList();
for (PrnfbButton candidate : this.settingsService.getButtons()) {
Map<PrnfbVariable, Supplier<String>> variables = getVariables(candidate.getUuid());
Map<PrnfbVariable, Supplier<String>> variables = getVariables(candidate.getUuid(), null);
PrnfbPullRequestAction pullRequestAction = BUTTON_TRIGGER;
if (this.userCheckService.isAllowedUseButton(candidate)//
&& isTriggeredByAction(clientKeyStore, notifications, shouldAcceptAnyCertificate, pullRequestAction, pullRequest,
Expand All @@ -102,11 +116,11 @@ && isTriggeredByAction(clientKeyStore, notifications, shouldAcceptAnyCertificate
allFoundButtons = usingToString().sortedCopy(allFoundButtons);
return allFoundButtons;
}

@VisibleForTesting
List<NotificationResponse> doHandlePressed(UUID buttonUuid, ClientKeyStore clientKeyStore,
boolean shouldAcceptAnyCertificate, final PullRequest pullRequest) {
Map<PrnfbVariable, Supplier<String>> variables = getVariables(buttonUuid);
boolean shouldAcceptAnyCertificate, final PullRequest pullRequest, final String formData) {
Map<PrnfbVariable, Supplier<String>> variables = getVariables(buttonUuid, formData);
List<NotificationResponse> successes = newArrayList();
for (PrnfbNotification prnfbNotification : this.settingsService.getNotifications()) {
PrnfbPullRequestAction pullRequestAction = BUTTON_TRIGGER;
Expand All @@ -126,10 +140,11 @@ List<NotificationResponse> doHandlePressed(UUID buttonUuid, ClientKeyStore clien
}

@VisibleForTesting
Map<PrnfbVariable, Supplier<String>> getVariables(final UUID uuid) {
Map<PrnfbVariable, Supplier<String>> getVariables(final UUID uuid, final String formData) {
Map<PrnfbVariable, Supplier<String>> variables = new HashMap<PrnfbVariable, Supplier<String>>();
PrnfbButton button = this.settingsService.getButton(uuid);
variables.put(BUTTON_TRIGGER_TITLE, Suppliers.ofInstance(button.getName()));
variables.put(BUTTON_FORM_DATA, Suppliers.ofInstance(formData));
return variables;
}

Expand Down
Loading

0 comments on commit 2a5c793

Please sign in to comment.