Compare commits

...

7 Commits

Author SHA1 Message Date
Sergey Ponomarev
6294835844 Fix NPE when FRAME_IMAGE not found
The FRAME_IMAGE is an app icon and it can be overridden via skin. It's ok that it's not found
2025-10-21 11:54:43 +02:00
Guus der Kinderen
20a5264c95 SPARK-2368 review feedback: fix casing of "Content-Type" 2025-10-21 11:39:14 +02:00
Guus der Kinderen
87b614f32c SPARK-2368: Proof-of-concept: display images
This is a very quick and dirty approach to rendering images.

This change bypasses imporant security checks.

Additional functionality is likely desired (such as: don't display images immediately, but show a button, which could prevent harm when abuse images are being received).
2025-10-21 11:39:14 +02:00
Guus der Kinderen
d2181c486d SPARK-2367: Improve performance of message reordering 2025-10-21 11:34:55 +02:00
Guus der Kinderen
eaa5e42819 SPARK-2371: Use a sensible timeout when waiting for a HTTP connection 2025-10-21 11:34:23 +02:00
Guus der Kinderen
27e1d35bce SPARK-2369: Prevent NPE in User-has-joined event 2025-10-21 11:29:13 +02:00
Guus der Kinderen
90defcc4bb SPARK-2370: Show chat room event messages
When a chat room is destroyed, the user in the room gets to see a message explaining what happened. Spark should not assume that the server will provide this message. Show a default message in case the server doesn’t sent one.
2025-10-21 11:25:38 +02:00
8 changed files with 198 additions and 54 deletions

View File

@ -203,20 +203,29 @@ public class Default {
// Otherwise, load and add to cache.
try {
final URL imageURL = getURL(imageName);
if (imageURL == null) {
Log.debug(imageName + " not found.");
return null;
}
final ImageIcon icon = new ImageIcon(imageURL);
cache.put(imageName, icon);
return icon;
}
catch (Exception e) {
Log.warning(imageName + " not found.", e);
Log.warning("Unable to load " + imageName, e);
}
return null;
}
public static URL getURL(String propertyName) {
URL pluginUrl = PluginRes.getDefaultURL(propertyName);
return pluginUrl != null ? pluginUrl : cl.getResource(getString(propertyName));
if (pluginUrl != null) return pluginUrl;
String resourceName = getString(propertyName);
if (resourceName == null) {
return null;
}
return cl.getResource(resourceName);
}

View File

@ -956,7 +956,9 @@ public class ChatContainer extends SparkTabbedPane implements MessageListener, C
-> {
for (final ChatRoomListener listener : chatRoomListeners) {
try {
listener.userHasJoined(room, userid.toString());
if (userid != null) {
listener.userHasJoined(room, userid.toString());
}
} catch (Exception e) {
Log.error("A ChatRoomListener (" + listener + ") threw an exception while processing a 'user joined' event for user '" + userid + "' in room: " + room, e);
}

View File

@ -15,16 +15,27 @@
*/
package org.jivesoftware.spark.ui;
import org.apache.hc.client5.http.config.RequestConfig;
import org.apache.hc.client5.http.impl.classic.CloseableHttpClient;
import org.apache.hc.client5.http.impl.classic.HttpClients;
import org.apache.hc.core5.http.ClassicHttpRequest;
import org.apache.hc.core5.http.HttpEntity;
import org.apache.hc.core5.http.io.support.ClassicRequestBuilder;
import org.jivesoftware.smack.SmackConfiguration;
import org.jivesoftware.spark.util.log.Log;
import org.jivesoftware.sparkimpl.plugin.emoticons.EmoticonManager;
import org.jivesoftware.sparkimpl.settings.local.SettingsManager;
import org.jivesoftware.sparkimpl.updater.AcceptAllCertsConnectionManager;
import javax.imageio.ImageIO;
import javax.swing.*;
import javax.swing.text.*;
import java.awt.*;
import java.awt.image.BufferedImage;
import java.time.ZonedDateTime;
import java.util.*;
import java.util.List;
import java.util.*;
import java.util.concurrent.TimeUnit;
import static javax.swing.text.StyleConstants.Foreground;
@ -41,11 +52,11 @@ import static javax.swing.text.StyleConstants.Foreground;
public class MessageEntry extends TimeStampedEntry
{
public static final List<Character> DIRECTIVE_CHARS = Arrays.asList( '*', '_', '~', '`' );
private final String prefix;
private final Color prefixColor;
private final String message;
private final Color messageColor;
private final Color backgroundColor;
protected final String prefix;
protected final Color prefixColor;
protected final String message;
protected final Color messageColor;
protected final Color backgroundColor;
/**
* Creates a new entry using the default background color (white/transparent).
@ -216,9 +227,10 @@ public class MessageEntry extends TimeStampedEntry
}
protected void insertFragment(ChatArea chatArea, String fragment, MutableAttributeSet style) throws BadLocationException {
if (insertPicture(chatArea, fragment, style)) return;
if (insertLink(chatArea.getDocument(), fragment, style)) return;
if (insertAddress(chatArea.getDocument(), fragment, style)) return;
if (insertImage(chatArea, fragment)) return;
if (insertEmoticon(chatArea, fragment)) return;
chatArea.getDocument().insertString(chatArea.getDocument().getLength(), fragment, style);
}
@ -349,6 +361,108 @@ public class MessageEntry extends TimeStampedEntry
return style;
}
/**
* Inserts a picture into the current document.
*
* @param url - the link to the content to insert( ex. http://example.org/hello.gif )
* @throws BadLocationException if the location is not available for insertion.
*/
public boolean insertPicture(ChatArea chatArea, String url, MutableAttributeSet messageStyle) throws BadLocationException
{
// FIXME: this is unsafe. Do not blindly accept anything that looks like an URL (check if it is a valid URL).
// TODO: instead of operating on message text content, operate on message stanza metadata.
// TODO: do not download each time. Cache downloaded data.
// TODO: make resized image clickable (open in unresized size).
if (url.startsWith("http://") ||
url.startsWith("https://")) {
try (final CloseableHttpClient httpClient =
HttpClients.custom()
.setConnectionManager(AcceptAllCertsConnectionManager.getInstance()) // FIXME: do not use acceptallcdertsconnectionmanager! It is unsafe. Only use trusted certificates!
.setDefaultRequestConfig(RequestConfig.custom().setResponseTimeout(SmackConfiguration.getDefaultReplyTimeout()/10, TimeUnit.MILLISECONDS).build())
.build()
) {
final ClassicHttpRequest request = ClassicRequestBuilder.get(url)
.setHeader("Content-Type", "image/*")
.setHeader("User-Agent", "Spark HttpFileUpload")
.build();
BufferedImage img = httpClient.execute(request, httpResponse -> {
HttpEntity entity = httpResponse.getEntity();
try {
return ImageIO.read(entity.getContent());
} catch (Throwable t) {
Log.warning("Unable to load picture from " + url, t);
return null;
}
});
if (img != null) {
SimpleAttributeSet center = new SimpleAttributeSet();
StyleConstants.setAlignment(center, StyleConstants.ALIGN_CENTER);
SimpleAttributeSet left = new SimpleAttributeSet();
StyleConstants.setAlignment(left, StyleConstants.ALIGN_LEFT);
final StyledDocument doc = (StyledDocument) chatArea.getDocument();
doc.insertString( doc.getLength(), "\n", messageStyle );
final int width = Math.max(60, Math.round(chatArea.getParent().getWidth()*0.70f));
final int height = Math.max(60, Math.round(chatArea.getParent().getHeight()*0.40f));
ImageIcon image = scaleImage(new ImageIcon(img), width, height);
int start = doc.getLength();
MutableAttributeSet inputAttributes = chatArea.getInputAttributes();
inputAttributes.removeAttributes(inputAttributes);
StyleConstants.setIcon(inputAttributes, image);
chatArea.getDocument().insertString(doc.getLength(), " ", chatArea.getInputAttributes());
inputAttributes.removeAttributes(inputAttributes);
doc.insertString(doc.getLength(), "\n", messageStyle);
final MutableAttributeSet linkStyle = new SimpleAttributeSet( messageStyle.copyAttributes() );
insertLink(doc, url, linkStyle);
int end = doc.getLength();
final int length = end-start+1;
doc.setParagraphAttributes(start, length, center, false);
// No longer center.
//System.out.println("text: " + doc.getText(start, length));
doc.setParagraphAttributes(doc.getLength()+2, 0, left,false);
return true;
}
} catch (Throwable e) {
Log.warning( "Unable to download content from " + url, e );
return false;
}
}
return false;
}
public static ImageIcon scaleImage(ImageIcon icon, int w, int h)
{
try {
int nw = icon.getIconWidth();
int nh = icon.getIconHeight();
if (icon.getIconWidth() > w) {
nw = w;
nh = (nw * icon.getIconHeight()) / icon.getIconWidth();
}
if (nh > h) {
nh = h;
nw = (icon.getIconWidth() * nh) / icon.getIconHeight();
}
return new ImageIcon(icon.getImage().getScaledInstance(nw, nh, Image.SCALE_DEFAULT));
} catch (Exception e) {
Log.warning("Unable to scale an image", e);
return null;
}
}
/**
* Inserts a link into the current document.
*
@ -403,7 +517,7 @@ public class MessageEntry extends TimeStampedEntry
* @param imageKey - the smiley representation of the image.( ex. :) )
* @return true if the image was found, otherwise false.
*/
public boolean insertImage( ChatArea chatArea, String imageKey )
public boolean insertEmoticon(ChatArea chatArea, String imageKey )
{
if ( !chatArea.getForceEmoticons() && !SettingsManager.getLocalPreferences().areEmoticonsEnabled() || !chatArea.emoticonsAvailable )
{

View File

@ -27,6 +27,7 @@ import org.jivesoftware.spark.SparkManager;
import org.jivesoftware.spark.plugin.ContextMenuListener;
import org.jivesoftware.spark.ui.history.HistoryWindow;
import org.jivesoftware.spark.util.ModelUtil;
import org.jivesoftware.spark.util.TaskEngine;
import org.jivesoftware.spark.util.log.Log;
import org.jivesoftware.sparkimpl.plugin.manager.Enterprise;
import org.jivesoftware.sparkimpl.plugin.emoticons.EmoticonManager;
@ -105,37 +106,50 @@ public class TranscriptWindow extends ChatArea implements ContextMenuListener
}
// Guarded by 'this'
boolean isReordingScheduled = false;
private synchronized void reOrder() {
// Clear and refill the UI component.
try
{
entries.sort( Comparator.comparing(TranscriptWindowEntry::isDelayed).thenComparing( TranscriptWindowEntry::getTimestamp ) );
clear();
for ( TranscriptWindowEntry e : entries )
{
e.addTo( this );
}
}
catch ( BadLocationException ex )
{
Log.error( "An exception prevented chat content to be redrawn in the user interface!", ex );
}
finally
{
isReordingScheduled = false;
}
}
protected synchronized void add( TranscriptWindowEntry entry )
{
final TranscriptWindow transcriptWindow = this;
//boolean reorderEverything = false;
if ( !entries.isEmpty() )
{
if ( entry.getTimestamp().isBefore( entries.getLast().getTimestamp() ) && !(entries.getLast() instanceof CustomTextEntry) )
{
Log.warning( "A chat entry appears to have been delivered out of order. The transcript window must be reordered!" );
//reorderEverything = true;
Log.debug( "A chat entry appears to have been delivered out of order. The transcript window must be reordered!" );
if (!isReordingScheduled) {
Log.warning( "Scheduling new re-ordering of entries in the transcript window." );
isReordingScheduled = true;
TaskEngine.getInstance().schedule(new TimerTask()
{
@Override
// This is an alternative approach to the 'reorderEverything boolean + routine. The idea behind using the
// SwingUtilities is to schedule redrawing after all currently queued messages (loads of which are
// probably also out of order), are processed, which should reduce the amount of redraws.
SwingUtilities.invokeLater( () ->
{
// Clear and refill the UI component.
try
{
entries.sort( Comparator.comparing(TranscriptWindowEntry::isDelayed).thenComparing( TranscriptWindowEntry::getTimestamp ) );
clear();
for ( TranscriptWindowEntry e : entries )
{
e.addTo( transcriptWindow );
}
}
catch ( BadLocationException ex )
{
Log.error( "An exception prevented chat content to be redrawn in the user interface!", ex );
}
} );
public void run()
{
SwingUtilities.invokeLater(() -> reOrder());
}
}, 250);
}
}
if ( !entry.getTimestamp().withZoneSameInstant( ZoneId.systemDefault() ).toLocalDate().isEqual( entries.getLast().getTimestamp().withZoneSameInstant( ZoneId.systemDefault() ).toLocalDate() ) )
{
@ -153,20 +167,7 @@ public class TranscriptWindow extends ChatArea implements ContextMenuListener
try
{
// if ( reorderEverything )
// {
// // Clear and refill the UI component.
// entries.sort( Comparator.comparing( TranscriptWindowEntry::getTimestamp ) );
// clear();
// for ( TranscriptWindowEntry e : entries )
// {
// e.addTo( this );
// }
// }
// else
{
entry.addTo( this );
}
entry.addTo( this );
}
catch ( BadLocationException ex )
{

View File

@ -128,7 +128,7 @@ public class ChatRoomImpl extends ChatRoom {
this.participantNickname = participantNickname;
// Loads the current history for this user.
loadHistory();
SwingUtilities.invokeLater(this::loadHistory);
// Register StanzaListeners
final StanzaFilter directFilter = new AndFilter(

View File

@ -735,7 +735,7 @@ public class GroupChatRoom extends ChatRoom
{
UIManager.put( "OptionPane.okButtonText", Res.getString( "ok" ) );
JOptionPane.showMessageDialog( this,
Res.getString( "message.room.destroyed", destroy.getReason() ),
(destroy.getReason() != null ? Res.getString( "message.room.destroyed", destroy.getReason() ) : Res.getString( "message.room.destroyed.no.reason" )),
Res.getString( "title.room.destroyed" ),
JOptionPane.INFORMATION_MESSAGE );
leaveChatRoom();
@ -782,7 +782,11 @@ public class GroupChatRoom extends ChatRoom
@Override
public void kicked( EntityFullJid participant, Jid actor, String reason )
{
insertText( Res.getString( "message.user.kicked.from.room", participant.getResourcepart(), actor, reason ) );
if (reason == null || reason.isEmpty()) {
insertText(Res.getString("message.user.kicked.from.room.no.reason", participant.getResourcepart(), actor));
} else {
insertText(Res.getString("message.user.kicked.from.room", participant.getResourcepart(), actor, reason));
}
}
@Override
@ -800,7 +804,11 @@ public class GroupChatRoom extends ChatRoom
@Override
public void banned( EntityFullJid participant, Jid actor, String reason )
{
insertText( Res.getString( "message.user.banned", participant.getResourcepart(), reason ) );
if (reason == null || reason.isEmpty()) {
insertText(Res.getString("message.user.banned.no.reason", participant.getResourcepart()));
} else {
insertText(Res.getString("message.user.banned", participant.getResourcepart(), reason));
}
}
@Override

View File

@ -15,6 +15,7 @@
*/
package org.jivesoftware.sparkimpl.updater;
import org.apache.hc.client5.http.config.ConnectionConfig;
import org.apache.hc.client5.http.impl.io.BasicHttpClientConnectionManager;
import org.apache.hc.client5.http.socket.ConnectionSocketFactory;
import org.apache.hc.client5.http.socket.PlainConnectionSocketFactory;
@ -22,13 +23,16 @@ import org.apache.hc.client5.http.ssl.NoopHostnameVerifier;
import org.apache.hc.client5.http.ssl.SSLConnectionSocketFactory;
import org.apache.hc.core5.http.config.Registry;
import org.apache.hc.core5.http.config.RegistryBuilder;
import org.apache.hc.core5.http.io.SocketConfig;
import org.apache.hc.core5.ssl.SSLContexts;
import org.apache.hc.core5.ssl.TrustStrategy;
import org.jivesoftware.smack.SmackConfiguration;
import javax.net.ssl.SSLContext;
import java.security.KeyManagementException;
import java.security.KeyStoreException;
import java.security.NoSuchAlgorithmException;
import java.util.concurrent.TimeUnit;
/**
* A HTTP Client connection manager that knowingly by-passes all verification of TLS certificates.
@ -68,6 +72,9 @@ public class AcceptAllCertsConnectionManager extends BasicHttpClientConnectionMa
.register("http", new PlainConnectionSocketFactory())
.build();
return new BasicHttpClientConnectionManager(socketFactoryRegistry);
final BasicHttpClientConnectionManager result = new BasicHttpClientConnectionManager(socketFactoryRegistry);
result.setSocketConfig(SocketConfig.custom().setSoTimeout(SmackConfiguration.getDefaultReplyTimeout(), TimeUnit.MILLISECONDS).build());
result.setConnectionConfig(ConnectionConfig.custom().setConnectTimeout(SmackConfiguration.getDefaultReplyTimeout(), TimeUnit.MILLISECONDS).build());
return result;
}
}

View File

@ -617,6 +617,7 @@ message.restart.spark.changes = Plugin will be removed on the next startup of Sp
message.restart.spark.to.install = You need to shut down the client to install latest version, would you like to do that now?
message.restart.required = You will need to restart Spark to have your changes take effect, restart now?
message.room.creation.error = The room could not be created
message.room.destroyed.no.reason = This room has been destroyed.
message.room.destroyed = This room has been destroyed due to the following reason: {0}
message.room.destruction.reason = Reason for destroying the room?
message.room.information.for = Room information for {0}
@ -670,6 +671,7 @@ message.unrecoverable.error = Unknown connection error. Please review the logs f
message.update.room.list = Update room list
message.updating.cancelled = Updating has been canceled
message.user.banned = {0} has been banned from this room. Reason: {1}
message.user.banned.no.reason = {0} has been banned from this room.
message.user.given.voice = {0} has been given a voice in this room
message.user.granted.admin = {0} has been granted administrator privileges
message.user.granted.membership = {0} has been given membership privileges
@ -678,6 +680,7 @@ message.user.granted.owner = {0} has been granted owner privileges
message.user.is.sending.you.a.file = {0} is sending you a file
message.user.joined.room = {0} has joined the room
message.user.kicked.from.room = {0} has been kicked out of the room by {1}. Reason: {2}
message.user.kicked.from.room.no.reason = {0} has been kicked out of the room by {1}.
message.user.left.room = {0} has left the room
message.user.nickname.changed = {0} is now known as {1}
message.user.now.available.to.chat = {0} is online at {1}