Skip to content

Commit 7709367

Browse files
committed
Refactor Snooze to use Dialog, external JS/CSS, and fix CSP/CSRF issues
1 parent ac35495 commit 7709367

File tree

10 files changed

+386
-87
lines changed

10 files changed

+386
-87
lines changed

core/src/main/java/hudson/model/AdministrativeMonitor.java

Lines changed: 99 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -57,33 +57,42 @@
5757
* monitoring and activate/deactivate the monitor accordingly. Sometimes
5858
* this can be done by updating a flag from code (see {@link SCMTrigger}
5959
* for one such example), while other times it's more convenient to do
60-
* so by running some code periodically (for this, use {@link TimerTrigger#timer})
60+
* so by running some code periodically (for this, use
61+
* {@link TimerTrigger#timer})
6162
*
6263
* <p>
63-
* {@link AdministrativeMonitor}s are bound to URL by {@link Jenkins#getAdministrativeMonitor(String)}.
64+
* {@link AdministrativeMonitor}s are bound to URL by
65+
* {@link Jenkins#getAdministrativeMonitor(String)}.
6466
* See {@link #getUrl()}.
6567
*
6668
* <h3>Views</h3>
6769
* <dl>
6870
* <dt>message.jelly</dt>
6971
* <dd>
70-
* If {@link #isActivated()} returns true, Jenkins will use the {@code message.jelly}
72+
* If {@link #isActivated()} returns true, Jenkins will use the
73+
* {@code message.jelly}
7174
* view of this object to render the warning text. This happens in the
7275
* {@code http://SERVER/jenkins/manage} page. This view should typically render
73-
* a DIV box with class='alert alert-danger' or class='alert alert-warning' with a human-readable text
76+
* a DIV box with class='alert alert-danger' or class='alert alert-warning' with
77+
* a human-readable text
7478
* inside it. It often also contains a link to a page that provides more details
7579
* about the problem.<br>
76-
* Additionally 2 numbers are shown in the Jenkins header of administrators, one with the number or active
77-
* non-security relevant monitors and one with the number of active security relevant monitors.
80+
* Additionally 2 numbers are shown in the Jenkins header of administrators, one
81+
* with the number or active
82+
* non-security relevant monitors and one with the number of active security
83+
* relevant monitors.
7884
* </dd>
7985
* </dl>
8086
*
8187
* <h3>Use with System Read permission</h3>
8288
* <p>
83-
* By default administrative monitors are visible only to users with Administer permission.
84-
* Users with {@link Jenkins#SYSTEM_READ} permission can access administrative monitors that override {@link #getRequiredPermission()}.
85-
* Care needs to be taken to ensure users with that permission don't have access to actions modifying system state.
86-
* For more details, see {@link #getRequiredPermission()}.
89+
* By default administrative monitors are visible only to users with Administer
90+
* permission.
91+
* Users with {@link Jenkins#SYSTEM_READ} permission can access administrative
92+
* monitors that override {@link #getRequiredPermission()}.
93+
* Care needs to be taken to ensure users with that permission don't have access
94+
* to actions modifying system state.
95+
* For more details, see {@link #getRequiredPermission()}.
8796
* </p>
8897
*
8998
* @author Kohsuke Kawaguchi
@@ -94,7 +103,8 @@ public abstract class AdministrativeMonitor extends AbstractModelObject implemen
94103
private static final Logger LOGGER = Logger.getLogger(AdministrativeMonitor.class.getName());
95104

96105
/**
97-
* Human-readable ID of this monitor, which needs to be unique within the system.
106+
* Human-readable ID of this monitor, which needs to be unique within the
107+
* system.
98108
*
99109
* <p>
100110
* This ID is used to remember persisted setting for this monitor,
@@ -111,7 +121,8 @@ protected AdministrativeMonitor() {
111121
}
112122

113123
/**
114-
* Returns the URL of this monitor, relative to the context path, like "administrativeMonitor/foobar".
124+
* Returns the URL of this monitor, relative to the context path, like
125+
* "administrativeMonitor/foobar".
115126
*/
116127
public String getUrl() {
117128
return "administrativeMonitor/" + id;
@@ -143,10 +154,12 @@ public void disable(boolean value) throws IOException {
143154
}
144155

145156
/**
146-
* Returns true if this monitor {@link #disable(boolean) isn't disabled} earlier.
157+
* Returns true if this monitor {@link #disable(boolean) isn't disabled}
158+
* earlier.
147159
*
148160
* <p>
149-
* This flag implements the ability for the admin to say "no thank you" to the monitor that
161+
* This flag implements the ability for the admin to say "no thank you" to the
162+
* monitor that
150163
* he wants to ignore.
151164
*/
152165
public boolean isEnabled() {
@@ -243,68 +256,100 @@ public void doDisable(StaplerRequest2 req, StaplerResponse2 rsp) throws IOExcept
243256
@RequirePOST
244257
public void doSnooze(StaplerRequest2 req, StaplerResponse2 rsp) throws IOException {
245258
Jenkins.get().checkPermission(Jenkins.ADMINISTER);
246-
String duration = req.getParameter("duration");
247259
long durationMs;
260+
248261
try {
249-
if ("custom".equals(duration)) {
262+
String preset = req.getParameter("durationPreset");
263+
if (preset == null) {
264+
throw new IllegalArgumentException("No duration specified");
265+
}
266+
267+
if ("custom".equals(preset)) {
250268
String minutesStr = req.getParameter("customMinutes");
251-
if (minutesStr == null) {
252-
throw new IllegalArgumentException("No customMinutes parameter");
269+
if (minutesStr == null || minutesStr.isEmpty()) {
270+
throw new IllegalArgumentException("Custom duration required");
253271
}
254272
long minutes = Long.parseLong(minutesStr);
255273
if (minutes <= 0) {
256-
throw new IllegalArgumentException("Custom minutes must be positive");
274+
throw new IllegalArgumentException("Minutes must be positive");
257275
}
258276
durationMs = minutes * 60 * 1000;
259277
} else {
260-
if (duration == null) {
261-
throw new IllegalArgumentException("No duration parameter");
262-
}
263-
durationMs = Long.parseLong(duration);
264-
if (durationMs <= 0) {
265-
throw new IllegalArgumentException("Duration must be positive");
278+
long minutes = Long.parseLong(preset);
279+
if (minutes <= 0) {
280+
throw new IllegalArgumentException("Invalid preset duration");
266281
}
282+
durationMs = minutes * 60 * 1000;
283+
}
284+
285+
// Validate max duration (1 year = 525600 minutes)
286+
if (durationMs > 525600L * 60 * 1000) {
287+
throw new IllegalArgumentException("Duration exceeds maximum (1 year)");
267288
}
289+
268290
} catch (IllegalArgumentException e) {
269291
rsp.sendError(StaplerResponse2.SC_BAD_REQUEST, e.getMessage());
270292
return;
271293
}
294+
272295
snooze(durationMs);
273-
rsp.sendRedirect2(req.getContextPath() + "/manage");
296+
297+
// AJAX: return 200; otherwise redirect
298+
if ("XMLHttpRequest".equals(req.getHeader("X-Requested-With"))) {
299+
rsp.setStatus(StaplerResponse2.SC_OK);
300+
} else {
301+
rsp.sendRedirect2(req.getContextPath() + "/manage");
302+
}
274303
}
275304

276305
/**
277306
* Required permission to view this admin monitor.
278-
* By default {@link Jenkins#ADMINISTER}, but {@link Jenkins#SYSTEM_READ} or {@link Jenkins#MANAGE} are also supported.
307+
* By default {@link Jenkins#ADMINISTER}, but {@link Jenkins#SYSTEM_READ} or
308+
* {@link Jenkins#MANAGE} are also supported.
279309
* <p>
280-
* Changing this permission check to return {@link Jenkins#SYSTEM_READ} will make the active
281-
* administrative monitor appear on {@link ManageJenkinsAction} to users without Administer permission.
282-
* {@link #doDisable(StaplerRequest2, StaplerResponse2)} will still always require Administer permission.
310+
* Changing this permission check to return {@link Jenkins#SYSTEM_READ} will
311+
* make the active
312+
* administrative monitor appear on {@link ManageJenkinsAction} to users without
313+
* Administer permission.
314+
* {@link #doDisable(StaplerRequest2, StaplerResponse2)} will still always
315+
* require Administer permission.
283316
* </p>
284317
* <p>
285-
* This method only allows for a single permission to be returned. If more complex permission checks are required,
286-
* override {@link #checkRequiredPermission()} and {@link #hasRequiredPermission()} instead.
318+
* This method only allows for a single permission to be returned. If more
319+
* complex permission checks are required,
320+
* override {@link #checkRequiredPermission()} and
321+
* {@link #hasRequiredPermission()} instead.
287322
* </p>
288323
* <p>
289-
* Implementers need to ensure that {@code doAct} and other web methods perform necessary permission checks:
290-
* Users with System Read permissions are expected to be limited to read-only access.
291-
* Form UI elements that change system state, e.g. toggling a feature on or off, need to be hidden from users
292-
* lacking Administer permission.
324+
* Implementers need to ensure that {@code doAct} and other web methods perform
325+
* necessary permission checks:
326+
* Users with System Read permissions are expected to be limited to read-only
327+
* access.
328+
* Form UI elements that change system state, e.g. toggling a feature on or off,
329+
* need to be hidden from users
330+
* lacking Administer permission.
293331
* </p>
332+
*
294333
* @since 2.233
295-
* @deprecated Callers should use {@link #checkRequiredPermission()} or {@link #hasRequiredPermission()}.
334+
* @deprecated Callers should use {@link #checkRequiredPermission()} or
335+
* {@link #hasRequiredPermission()}.
296336
*/
297337
@Deprecated
298338
public Permission getRequiredPermission() {
299339
return Jenkins.ADMINISTER;
300340
}
301341

302342
/**
303-
* Checks if the current user has the minimum required permission to view this administrative monitor.
343+
* Checks if the current user has the minimum required permission to view this
344+
* administrative monitor.
304345
* <p>
305-
* Subclasses may override this method and {@link #hasRequiredPermission()} instead of {@link #getRequiredPermission()} to perform more complex permission checks,
306-
* for example, checking either {@link Jenkins#MANAGE} or {@link Jenkins#SYSTEM_READ}.
346+
* Subclasses may override this method and {@link #hasRequiredPermission()}
347+
* instead of {@link #getRequiredPermission()} to perform more complex
348+
* permission checks,
349+
* for example, checking either {@link Jenkins#MANAGE} or
350+
* {@link Jenkins#SYSTEM_READ}.
307351
* </p>
352+
*
308353
* @see #getRequiredPermission()
309354
* @see #hasRequiredPermission()
310355
* @since 2.468
@@ -314,11 +359,16 @@ public void checkRequiredPermission() {
314359
}
315360

316361
/**
317-
* Checks if the current user has the minimum required permission to view this administrative monitor.
362+
* Checks if the current user has the minimum required permission to view this
363+
* administrative monitor.
318364
* <p>
319-
* Subclasses may override this method and {@link #checkRequiredPermission} instead of {@link #getRequiredPermission()} to perform more complex permission checks,
320-
* for example, checking either {@link Jenkins#MANAGE} or {@link Jenkins#SYSTEM_READ}.
365+
* Subclasses may override this method and {@link #checkRequiredPermission}
366+
* instead of {@link #getRequiredPermission()} to perform more complex
367+
* permission checks,
368+
* for example, checking either {@link Jenkins#MANAGE} or
369+
* {@link Jenkins#SYSTEM_READ}.
321370
* </p>
371+
*
322372
* @see #getRequiredPermission()
323373
* @see #checkRequiredPermission()
324374
* @since 2.468
@@ -328,9 +378,11 @@ public boolean hasRequiredPermission() {
328378
}
329379

330380
/**
331-
* Checks if the current user has the minimum required permission to view any administrative monitor.
381+
* Checks if the current user has the minimum required permission to view any
382+
* administrative monitor.
332383
*
333-
* @return true if the current user has the minimum required permission to view any administrative monitor.
384+
* @return true if the current user has the minimum required permission to view
385+
* any administrative monitor.
334386
*
335387
* @since 2.468
336388
*/
@@ -339,7 +391,8 @@ public static boolean hasPermissionToDisplay() {
339391
}
340392

341393
/**
342-
* Ensure that URLs in this administrative monitor are only accessible to users with {@link #getRequiredPermission()}.
394+
* Ensure that URLs in this administrative monitor are only accessible to users
395+
* with {@link #getRequiredPermission()}.
343396
*/
344397
@Override
345398
@Restricted(NoExternalUse.class)

core/src/main/resources/hudson/model/Messages.properties

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -427,8 +427,3 @@ ManagementLink.Category.UNCATEGORIZED=Uncategorized
427427
FileParameterValue.IndexTitle=File Parameters
428428

429429
UserPreferencesProperty.DisplayName=Preferences
430-
431-
snooze=Snooze
432-
snooze.duration.label=Snooze duration in minutes
433-
snooze.duration.placeholder=Minutes
434-
snooze.duration.title=Enter duration in minutes (max 525600 = 1 year)

core/src/main/resources/jenkins/diagnostics/ControllerExecutorsAgents/message.jelly

Lines changed: 4 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -24,24 +24,16 @@ THE SOFTWARE.
2424

2525
<?jelly escape-by-default='true'?>
2626
<j:jelly xmlns:j="jelly:core" xmlns:st="jelly:stapler" xmlns:d="jelly:define" xmlns:l="/lib/layout" xmlns:t="/lib/hudson" xmlns:f="/lib/form">
27-
<div class="jenkins-alert jenkins-alert-warning" style="display: flex; justify-content: space-between; align-items: center; gap: 1rem;">
27+
<div class="jenkins-alert jenkins-alert-warning">
2828
<div>
2929
${%ExecutorsWarning}
3030
</div>
31-
<div style="display: flex; flex-wrap: wrap; gap: 0.5rem; align-items: center;">
32-
<form method="post" action="${rootURL}/${it.url}/act" name="${it.id}" style="display: inline-flex; gap: 0.5rem;">
31+
<div class="snooze-form">
32+
<form method="post" action="${rootURL}/${it.url}/act" name="${it.id}" class="snooze-form">
3333
<f:submit name="yes" value="${%Manage}"/>
3434
<f:submit name="no" value="${%Dismiss}"/>
3535
</form>
36-
<form method="post" action="${rootURL}/${it.url}/snooze" style="display: inline-flex; gap: 0.5rem; align-items: center;">
37-
<j:if test="${h.isCrumbEncoded()}">
38-
<input type="hidden" name="${h.crumbRequestField}" value="${h.getCrumb(request)}"/>
39-
</j:if>
40-
<input type="hidden" name="duration" value="custom"/>
41-
<label for="snooze-minutes-${it.id}" class="jenkins-visually-hidden">${%snooze.duration.label}</label>
42-
<input type="number" id="snooze-minutes-${it.id}" name="customMinutes" placeholder="${%snooze.duration.placeholder}" min="1" max="525600" step="1" style="width: 80px;" class="jenkins-input" required="required" aria-label="${%snooze.duration.label}" title="${%snooze.duration.title}"/>
43-
<f:submit value="${%snooze}"/>
44-
</form>
36+
<t:snoozeForm />
4537
</div>
4638
</div>
4739
</j:jelly>

core/src/main/resources/jenkins/diagnostics/ControllerExecutorsNoAgents/message.jelly

Lines changed: 4 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -24,25 +24,17 @@ THE SOFTWARE.
2424

2525
<?jelly escape-by-default='true'?>
2626
<j:jelly xmlns:j="jelly:core" xmlns:st="jelly:stapler" xmlns:d="jelly:define" xmlns:l="/lib/layout" xmlns:t="/lib/hudson" xmlns:f="/lib/form">
27-
<div class="jenkins-alert jenkins-alert-warning" style="display: flex; justify-content: space-between; align-items: center; gap: 1rem;">
27+
<div class="jenkins-alert jenkins-alert-warning">
2828
<div>
2929
${%ExecutorsWarning}
3030
</div>
31-
<div style="display: flex; flex-wrap: wrap; gap: 0.5rem; align-items: center;">
32-
<form method="post" action="${rootURL}/${it.url}/act" name="${it.id}" style="display: inline-flex; gap: 0.5rem;">
31+
<div class="snooze-form">
32+
<form method="post" action="${rootURL}/${it.url}/act" name="${it.id}" class="snooze-form">
3333
<f:submit name="agent" value="${%Set up agent}"/>
3434
<f:submit name="cloud" value="${%Set up cloud}"/>
3535
<f:submit name="no" value="${%Dismiss}"/>
3636
</form>
37-
<form method="post" action="${rootURL}/${it.url}/snooze" style="display: inline-flex; gap: 0.5rem; align-items: center;">
38-
<j:if test="${h.isCrumbEncoded()}">
39-
<input type="hidden" name="${h.crumbRequestField}" value="${h.getCrumb(request)}"/>
40-
</j:if>
41-
<input type="hidden" name="duration" value="custom"/>
42-
<label for="snooze-minutes-${it.id}" class="jenkins-visually-hidden">${%snooze.duration.label}</label>
43-
<input type="number" id="snooze-minutes-${it.id}" name="customMinutes" placeholder="${%snooze.duration.placeholder}" min="1" max="525600" step="1" style="width: 80px;" class="jenkins-input" required="required" aria-label="${%snooze.duration.label}" title="${%snooze.duration.title}"/>
44-
<f:submit value="${%snooze}"/>
45-
</form>
37+
<t:snoozeForm />
4638
</div>
4739
</div>
4840
</j:jelly>
Lines changed: 4 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,15 @@
11
<?jelly escape-by-default='true'?>
22
<j:jelly xmlns:j="jelly:core" xmlns:st="jelly:stapler" xmlns:d="jelly:define" xmlns:l="/lib/layout" xmlns:t="/lib/hudson" xmlns:f="/lib/form">
3-
<div class="jenkins-alert jenkins-alert-warning" style="display: flex; justify-content: space-between; align-items: center; gap: 1rem;">
3+
<div class="jenkins-alert jenkins-alert-warning">
44
<div>
55
${%blurb}
66
</div>
7-
<div style="display: flex; flex-wrap: wrap; gap: 0.5rem; align-items: center;">
8-
<form method="post" action="${rootURL}/${it.url}/act" name="${it.id}" style="display: inline-flex; gap: 0.5rem;">
7+
<div class="snooze-form">
8+
<form method="post" action="${rootURL}/${it.url}/act" name="${it.id}" class="snooze-form">
99
<f:submit name="yes" value="${%Apply Migration}"/>
1010
<f:submit name="no" value="${%Dismiss}"/>
1111
</form>
12-
<form method="post" action="${rootURL}/${it.url}/snooze" style="display: inline-flex; gap: 0.5rem; align-items: center;">
13-
<j:if test="${h.isCrumbEncoded()}">
14-
<input type="hidden" name="${h.crumbRequestField}" value="${h.getCrumb(request)}"/>
15-
</j:if>
16-
<input type="hidden" name="duration" value="custom"/>
17-
<label for="snooze-minutes-${it.id}" class="jenkins-visually-hidden">${%snooze.duration.label}</label>
18-
<input type="number" id="snooze-minutes-${it.id}" name="customMinutes" placeholder="${%snooze.duration.placeholder}" min="1" max="525600" step="1" style="width: 80px;" class="jenkins-input" required="required" aria-label="${%snooze.duration.label}" title="${%snooze.duration.title}"/>
19-
<f:submit value="${%snooze}"/>
20-
</form>
12+
<t:snoozeForm />
2113
</div>
2214
</div>
2315
</j:jelly>
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
.snooze-dialog {
2+
border: 1px solid var(--panel-border-color, #dbdbdb);
3+
border-radius: 6px;
4+
padding: 1.5rem;
5+
box-shadow: 0 5px 15px rgba(0, 0, 0, 0.2);
6+
max-width: 500px;
7+
background: var(--panel-background, #ffffff);
8+
color: var(--text-color, #0b0b0b);
9+
}
10+
11+
.snooze-dialog::backdrop {
12+
background: rgba(0, 0, 0, 0.4);
13+
}
14+
15+
.snooze-dialog:not([open]) {
16+
display: none;
17+
}
18+
19+
.snooze-dialog__title {
20+
margin-top: 0;
21+
margin-bottom: 1rem;
22+
font-size: 1.25rem;
23+
font-weight: bold;
24+
}
25+
26+
.snooze-dialog__contents {
27+
margin-bottom: 1.5rem;
28+
}
29+
30+
.snooze-dialog__buttons {
31+
display: flex;
32+
gap: 10px;
33+
justify-content: flex-end;
34+
}
35+
36+
/* Override core alert form styles */
37+
.manage-messages .alert .snooze-dialog form {
38+
float: none;
39+
margin: 0 !important;
40+
display: block;
41+
}

0 commit comments

Comments
 (0)