Skip to content

Commit d34418c

Browse files
committed
commonlib: Provide tabbed output panel
Replace the default output panel with a tabbed version for multiple output sources. Signed-off-by: ricekot <git@ricekot.com>
1 parent 124016c commit d34418c

File tree

9 files changed

+349
-1
lines changed

9 files changed

+349
-1
lines changed

addOns/commonlib/CHANGELOG.md

+1
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
77
## Unreleased
88
### Added
99
- Add solutions to Insufficient Process Validation vulnerability (Issue 8056).
10+
- Replace the default Output panel with a tabbed version to allow multiple sources of output to be displayed in separate tabs.
1011

1112
### Changed
1213
- Update minimum ZAP version to 2.16.0.

addOns/commonlib/src/main/java/org/zaproxy/addon/commonlib/ExtensionCommonlib.java

+5
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@
3030
import org.parosproxy.paros.model.Session;
3131
import org.zaproxy.addon.commonlib.internal.vulns.LegacyVulnerabilities;
3232
import org.zaproxy.addon.commonlib.ui.ProgressPanel;
33+
import org.zaproxy.addon.commonlib.ui.TabbedOutputPanel;
3334

3435
public class ExtensionCommonlib extends ExtensionAdaptor {
3536

@@ -109,6 +110,7 @@ public ExtensionCommonlib() {
109110
public void hook(ExtensionHook extensionHook) {
110111
if (hasView()) {
111112
extensionHook.getHookView().addStatusPanel(getProgressPanel());
113+
getView().setOutputPanel(new TabbedOutputPanel());
112114
}
113115
extensionHook.addSessionListener(new SessionChangedListenerImpl());
114116
}
@@ -127,6 +129,9 @@ public boolean canUnload() {
127129

128130
@Override
129131
public void unload() {
132+
if (hasView()) {
133+
getView().setOutputPanel(null);
134+
}
130135
LegacyVulnerabilities.unload();
131136
}
132137

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,305 @@
1+
/*
2+
* Zed Attack Proxy (ZAP) and its related class files.
3+
*
4+
* ZAP is an HTTP/HTTPS proxy for assessing web application security.
5+
*
6+
* Copyright 2024 The ZAP Development Team
7+
*
8+
* Licensed under the Apache License, Version 2.0 (the "License");
9+
* you may not use this file except in compliance with the License.
10+
* You may obtain a copy of the License at
11+
*
12+
* http://www.apache.org/licenses/LICENSE-2.0
13+
*
14+
* Unless required by applicable law or agreed to in writing, software
15+
* distributed under the License is distributed on an "AS IS" BASIS,
16+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
17+
* See the License for the specific language governing permissions and
18+
* limitations under the License.
19+
*/
20+
package org.zaproxy.addon.commonlib.ui;
21+
22+
import java.awt.BorderLayout;
23+
import java.awt.Component;
24+
import java.awt.event.KeyEvent;
25+
import java.util.ArrayList;
26+
import java.util.Comparator;
27+
import java.util.HashMap;
28+
import java.util.List;
29+
import java.util.Map;
30+
import javax.swing.AbstractButton;
31+
import javax.swing.Icon;
32+
import javax.swing.ImageIcon;
33+
import javax.swing.JButton;
34+
import javax.swing.JPanel;
35+
import javax.swing.JScrollPane;
36+
import javax.swing.JToolBar;
37+
import javax.swing.text.DefaultCaret;
38+
import org.apache.commons.lang3.exception.ExceptionUtils;
39+
import org.parosproxy.paros.Constant;
40+
import org.parosproxy.paros.extension.AbstractPanel;
41+
import org.parosproxy.paros.model.Model;
42+
import org.parosproxy.paros.view.OutputPanel;
43+
import org.parosproxy.paros.view.View;
44+
import org.zaproxy.zap.extension.help.ExtensionHelp;
45+
import org.zaproxy.zap.utils.DisplayUtils;
46+
import org.zaproxy.zap.utils.ThreadUtils;
47+
import org.zaproxy.zap.utils.TimeStampUtils;
48+
import org.zaproxy.zap.utils.ZapTextArea;
49+
import org.zaproxy.zap.view.OutputSource;
50+
import org.zaproxy.zap.view.TabbedPanel2;
51+
import org.zaproxy.zap.view.ZapToggleButton;
52+
53+
/**
54+
* A tabbed version of the output panel that allows multiple sources of output to be displayed in
55+
* separate tabs.
56+
*
57+
* @since 1.30.0
58+
*/
59+
@SuppressWarnings("serial")
60+
public class TabbedOutputPanel extends OutputPanel {
61+
62+
public static final String ATTRIBUTE_ICON = "commonlib.output.panel.icon";
63+
public static final String ATTRIBUTE_ADDITIONAL_BUTTONS =
64+
"commonlib.output.panel.additionalButtons";
65+
66+
private static final String DEFAULT_OUTPUT_SOURCE_NAME =
67+
Constant.messages.getString("commonlib.output.panel.default");
68+
private static final String ERROR_OUTPUT_SOURCE_NAME =
69+
Constant.messages.getString("commonlib.output.panel.error");
70+
71+
private static final String CLEAR_BUTTON_TOOL_TIP =
72+
Constant.messages.getString("commonlib.output.panel.button.clear.toolTip");
73+
74+
private static final ImageIcon DOC_ICON = getImageIcon("/resource/icon/16/172.png");
75+
private static final ImageIcon BROOM_ICON = getImageIcon("/resource/icon/fugue/broom.png");
76+
private static final ImageIcon SCROLL_LOCK_DISABLED_ICON =
77+
getImageIcon("/org/zaproxy/addon/commonlib/resources/ui-scroll-pane.png");
78+
private static final ImageIcon SCROLL_LOCK_ENABLED_ICON =
79+
getImageIcon("/org/zaproxy/addon/commonlib/resources/ui-scroll-lock-pane.png");
80+
81+
private final TabbedPanel2 tabbedPanel;
82+
83+
private final Map<String, ZapTextArea> txtOutputs;
84+
private final Map<String, OutputSource> registeredOutputSources;
85+
86+
public TabbedOutputPanel() {
87+
txtOutputs = new HashMap<>();
88+
registeredOutputSources = new HashMap<>();
89+
90+
setLayout(new BorderLayout());
91+
setName(Constant.messages.getString("commonlib.output.panel.title"));
92+
setIcon(DOC_ICON);
93+
setDefaultAccelerator(
94+
View.getSingleton()
95+
.getMenuShortcutKeyStroke(KeyEvent.VK_O, KeyEvent.SHIFT_DOWN_MASK, false));
96+
setMnemonic(Constant.messages.getChar("commonlib.output.panel.mnemonic"));
97+
98+
tabbedPanel = new TabbedPanel2();
99+
addNewOutputSource(DEFAULT_OUTPUT_SOURCE_NAME);
100+
101+
var mainPanel = new JPanel(new BorderLayout());
102+
mainPanel.add(tabbedPanel, BorderLayout.CENTER);
103+
add(mainPanel, BorderLayout.CENTER);
104+
105+
setShowByDefault(true);
106+
107+
ExtensionHelp.enableHelpKey(this, "commonlib.output.panel");
108+
}
109+
110+
@Override
111+
public void registerOutputSource(OutputSource source) {
112+
registeredOutputSources.put(source.getName(), source);
113+
}
114+
115+
@Override
116+
public void unregisterOutputSource(OutputSource source) {
117+
if (txtOutputs.containsKey(source.getName())) {
118+
for (Component tab : tabbedPanel.getTabList()) {
119+
if (tab.getName().equals(source.getName())) {
120+
tabbedPanel.removeTab((AbstractPanel) tab);
121+
break;
122+
}
123+
}
124+
txtOutputs.remove(source.getName());
125+
}
126+
registeredOutputSources.remove(source.getName());
127+
}
128+
129+
private void addNewOutputSource(String name) {
130+
if (txtOutputs.containsKey(name)) {
131+
return;
132+
}
133+
Map<String, Object> attributes =
134+
registeredOutputSources.containsKey(name)
135+
? registeredOutputSources.get(name).getAttributes()
136+
: Map.of();
137+
138+
var outputPanel = new AbstractPanel();
139+
outputPanel.setName(name);
140+
outputPanel.setLayout(new BorderLayout());
141+
if (attributes.containsKey(ATTRIBUTE_ICON)
142+
&& attributes.get(ATTRIBUTE_ICON) instanceof Icon) {
143+
outputPanel.setIcon((Icon) attributes.get(ATTRIBUTE_ICON));
144+
}
145+
146+
ZapTextArea txtOutput = buildOutputTextArea();
147+
JToolBar toolBar = buildToolbar(txtOutput, attributes);
148+
outputPanel.add(toolBar, BorderLayout.PAGE_START);
149+
var jScrollPane = new JScrollPane();
150+
jScrollPane.setViewportView(txtOutput);
151+
jScrollPane.setHorizontalScrollBarPolicy(JScrollPane.HORIZONTAL_SCROLLBAR_NEVER);
152+
outputPanel.add(jScrollPane, BorderLayout.CENTER);
153+
154+
boolean hideable = !DEFAULT_OUTPUT_SOURCE_NAME.equals(name);
155+
boolean visible = tabbedPanel.getTabCount() < 8;
156+
tabbedPanel.addTab(name, outputPanel.getIcon(), outputPanel, hideable, visible, -1);
157+
txtOutputs.put(name, txtOutput);
158+
}
159+
160+
private static ZapTextArea buildOutputTextArea() {
161+
var txtOutput = new ZapTextArea();
162+
txtOutput.setEditable(false);
163+
txtOutput.setLineWrap(true);
164+
txtOutput.setName("");
165+
txtOutput.addMouseListener(
166+
new java.awt.event.MouseAdapter() {
167+
@Override
168+
public void mousePressed(java.awt.event.MouseEvent e) {
169+
showPopupMenuIfTriggered(e);
170+
}
171+
172+
@Override
173+
public void mouseReleased(java.awt.event.MouseEvent e) {
174+
showPopupMenuIfTriggered(e);
175+
}
176+
177+
private void showPopupMenuIfTriggered(java.awt.event.MouseEvent e) {
178+
if (e.isPopupTrigger()) {
179+
View.getSingleton()
180+
.getPopupMenu()
181+
.show(e.getComponent(), e.getX(), e.getY());
182+
}
183+
}
184+
});
185+
return txtOutput;
186+
}
187+
188+
private static JToolBar buildToolbar(ZapTextArea txtOutput, Map<String, Object> attributes) {
189+
List<AbstractButton> buttons = new ArrayList<>();
190+
191+
JButton clearButton = new JButton();
192+
clearButton.setName("clearButton");
193+
clearButton.setToolTipText(CLEAR_BUTTON_TOOL_TIP);
194+
clearButton.setIcon(BROOM_ICON);
195+
clearButton.addActionListener(e -> txtOutput.setText(""));
196+
buttons.add(clearButton);
197+
198+
ZapToggleButton scrollLockButton = new ZapToggleButton();
199+
scrollLockButton.setName("scrollLockButton");
200+
scrollLockButton.setToolTipText(
201+
Constant.messages.getString(
202+
"commonlib.output.panel.button.scrolllock.disabled.toolTip"));
203+
scrollLockButton.setSelectedToolTipText(
204+
Constant.messages.getString(
205+
"commonlib.output.panel.button.scrolllock.enabled.toolTip"));
206+
scrollLockButton.setIcon(DisplayUtils.getScaledIcon(SCROLL_LOCK_DISABLED_ICON));
207+
scrollLockButton.setSelectedIcon(DisplayUtils.getScaledIcon(SCROLL_LOCK_ENABLED_ICON));
208+
scrollLockButton.addActionListener(
209+
e -> {
210+
if (scrollLockButton.isSelected()) {
211+
DefaultCaret caret = (DefaultCaret) txtOutput.getCaret();
212+
caret.setUpdatePolicy(DefaultCaret.NEVER_UPDATE);
213+
} else {
214+
DefaultCaret caret = (DefaultCaret) txtOutput.getCaret();
215+
caret.setUpdatePolicy(DefaultCaret.ALWAYS_UPDATE);
216+
txtOutput.setCaretPosition(txtOutput.getDocument().getLength());
217+
}
218+
});
219+
buttons.add(scrollLockButton);
220+
221+
if (attributes.containsKey(ATTRIBUTE_ADDITIONAL_BUTTONS)
222+
&& attributes.get(ATTRIBUTE_ADDITIONAL_BUTTONS) instanceof List) {
223+
((List<?>) attributes.get(ATTRIBUTE_ADDITIONAL_BUTTONS))
224+
.stream()
225+
.filter(button -> button instanceof AbstractButton)
226+
.forEach(button -> buttons.add((AbstractButton) button));
227+
}
228+
229+
var toolBar = new JToolBar();
230+
toolBar.setEnabled(true);
231+
toolBar.setFloatable(false);
232+
toolBar.setRollover(true);
233+
buttons.stream()
234+
.sorted(
235+
Comparator.comparing(
236+
Component::getName,
237+
Comparator.nullsLast(Comparator.naturalOrder())))
238+
.forEach(toolBar::add);
239+
return toolBar;
240+
}
241+
242+
@Override
243+
public void append(final String msg) {
244+
append(msg, DEFAULT_OUTPUT_SOURCE_NAME);
245+
}
246+
247+
@Override
248+
public void append(String msg, String sourceName) {
249+
if (!txtOutputs.containsKey(sourceName)) {
250+
addNewOutputSource(sourceName);
251+
}
252+
ThreadUtils.invokeAndWaitHandled(() -> doAppend(txtOutputs.get(sourceName), msg));
253+
}
254+
255+
@Override
256+
public void append(final Exception e) {
257+
append(ExceptionUtils.getStackTrace(e), ERROR_OUTPUT_SOURCE_NAME);
258+
}
259+
260+
@Override
261+
public void appendAsync(final String message) {
262+
appendAsync(message, DEFAULT_OUTPUT_SOURCE_NAME);
263+
}
264+
265+
@Override
266+
public void appendAsync(String message, String sourceName) {
267+
ThreadUtils.invokeLater(() -> append(message, sourceName));
268+
}
269+
270+
@Override
271+
public void clear() {
272+
tabbedPanel.removeAll();
273+
txtOutputs.clear();
274+
addNewOutputSource(DEFAULT_OUTPUT_SOURCE_NAME);
275+
}
276+
277+
@Override
278+
public void clear(String sourceName) {
279+
if (txtOutputs.containsKey(sourceName)) {
280+
txtOutputs.get(sourceName).setText("");
281+
}
282+
}
283+
284+
private void doAppend(ZapTextArea txtOutput, String message) {
285+
if (Model.getSingleton()
286+
.getOptionsParam()
287+
.getViewParam()
288+
.isOutputTabTimeStampingEnabled()) {
289+
txtOutput.append(
290+
TimeStampUtils.getTimeStampedMessage(
291+
message,
292+
Model.getSingleton()
293+
.getOptionsParam()
294+
.getViewParam()
295+
.getOutputTabTimeStampsFormat()));
296+
} else {
297+
txtOutput.append(message);
298+
}
299+
}
300+
301+
private static ImageIcon getImageIcon(String resourceName) {
302+
return DisplayUtils.getScaledIcon(
303+
new ImageIcon(TabbedOutputPanel.class.getResource(resourceName)));
304+
}
305+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 3.2 Final//EN">
2+
<html>
3+
<head>
4+
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
5+
<title>
6+
Tabbed Output Panel
7+
</title>
8+
</head>
9+
<body>
10+
<h1>Tabbed Output Panel</h1>
11+
12+
The Common Library add-on provides a tabbed output panel that replaces the default output panel in ZAP.
13+
This allows other add-ons to optionally log messages under named tabs in the output panel.
14+
If a name is not provided, messages are shown under the "General" tab and exceptions are shown under the "Errors" tab
15+
by default.
16+
17+
<p>
18+
While the Output tab itself can be detached, the tabs within the Output tab can also be detached if desired.
19+
To detach a tab, right click on it and select "Move Tab to New Window" in the context menu that pops up.
20+
21+
<p>
22+
Right-clicking within the body of a tab will display a context menu with relevant options such as Copy and Find.
23+
All standard shortcuts (such as Ctrl/Cmd + A/C/F), should work as expected too.
24+
25+
</body>
26+
</html>

addOns/commonlib/src/main/javahelp/help/map.jhm

+1
Original file line numberDiff line numberDiff line change
@@ -5,4 +5,5 @@
55

66
<map version="1.0">
77
<mapID target="commonlib" url="contents/commonlib.html" />
8+
<mapID target="commonlib.output.panel" url="contents/output-panel.html" />
89
</map>

addOns/commonlib/src/main/javahelp/help/toc.xml

+3-1
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,9 @@
66
<toc version="2.0">
77
<tocitem text="ZAP User Guide" tocid="toplevelitem">
88
<tocitem text="Add Ons" tocid="addons">
9-
<tocitem text="Common Library" target="commonlib"/>
9+
<tocitem text="Common Library" target="commonlib">
10+
<tocitem text="Tabbed Output Panel" target="commonlib.output.panel" />
11+
</tocitem>
1012
</tocitem>
1113
</tocitem>
1214
</toc>

addOns/commonlib/src/main/resources/org/zaproxy/addon/commonlib/resources/Messages.properties

+8
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,14 @@ commonlib.desc = A library of shared functionality
22

33
commonlib.name = Common Library
44

5+
commonlib.output.panel.button.clear.toolTip = Clear Output Panel
6+
commonlib.output.panel.button.scrolllock.disabled.toolTip = Enable scroll lock
7+
commonlib.output.panel.button.scrolllock.enabled.toolTip = Disable scroll lock
8+
commonlib.output.panel.default = General
9+
commonlib.output.panel.error = Errors
10+
commonlib.output.panel.mnemonic = o
11+
commonlib.output.panel.title = Output
12+
513
commonlib.progress.pane.completed = Completed.
614
commonlib.progress.pane.status = Status: {0} out of {1} tasks processed
715
commonlib.progress.pane.title = Progress
Loading

0 commit comments

Comments
 (0)