This post is long overdue. I originally planned to write it to document the issues and solutions while working on the app, but as the problems kept appearing I wanted to make sure that I have all the answers. Now I know that I probably won't have them in the near future as I'm seeing new issues even after having the app in production for some time.
The project was about a webization of an existing app that served well in Notes client UI for many years. It's a classic support/helpdesk app that once used to be a great example of Notes flexibility and integration of mail functions with app code. After all the mail database was just another app using the same APIs (mostly). Unfortunately, this is not the case for XPages as iNotes or Verse has nothing in common with XPages, so the APIs are not battle-tested for all the scenarios. I remember doing a similar project during the early days of XPages and I must admit that it was more painful, but mostly due to a lack of community resources and experience. Some of the issues that I'll be mentioning are fixed in the upcoming 12.0.2 version, so it may get easier to maintain the app eventually.
There are still many great blog posts about XPages, RichText and MIME issues, so I'll try to refer to them if possible. Unfortunately, some of the old blogs are no longer available, which means that links e.g. from StackOverflow answers are no longer helpful.
Before starting the project we did a proof-of-concept to see if it was even possible to implement all the mail-related functions in a way that will work for the users. The app uses all classic mail functions like reply to all with history (and without attachment) and needs to be backward compatible, so the code must work for existing RichText items and new data stored in MIME. The PoC was successful, so we started the implementation.
I won't be mentioning the issues in the order as we have discovered them, but rather based on their impact on the final (currently) solution.
Problem 1 - To ACF or not to ACF
Active-content-filtering is a really important feature for security, especially when we are working with emails from the Internet. We had ACF enabled (in the default) settings during the development and testing of the application. It worked well. Unfortunately, once we deployed the app to production, we started to see issues with some received emails.
ACF crashing on semi-invalid messages
The first problem that we encountered was caused by invalid inline image data in emails. The details are not that important, simply the image claimed to have inline base-64 value, but there was none. Something like:
<img src=3D"data:image/png;base64,cid=
:116c8444-683e-423d-964a-0b914d8e4b01" style=3D"position: static !importa=
nt;" title=3D"Call: " />
The result was ACF completely crashing in a way that the page was not loaded:
Exception
Error while executing active content filter
Bad character exits in Base64 array
Array index out of range: -1
com.ibm.xsp.FacesExceptionEx: Error while executing active content filter
com.ibm.xsp.acf.ACFProcessor._processMarkup(ACFProcessor.java:106)
com.ibm.xsp.acf.ACFProcessor.processMarkup(ACFProcessor.java:92)
com.ibm.xsp.context.FacesContextExImpl.filterHtml(FacesContextExImpl.java:928)
com.ibm.xsp.util.FacesUtil.convertValue(FacesUtil.java:1152
So the users were not able to even see what was in that email.
Workaround
Custom ReadOnlyRteRenderer with improved error handling. This way we can adjust HTML if there is an error - but it's really a try-and-guess approach to find the problem.
public class ReadOnlyRteRenderer extends com.ibm.xsp.renderkit.html_basic.ReadOnlyRteRenderer { @Override protected String encodeText(FacesContext paramFacesContext, UIComponent paramUIComponent) { try { return super.encodeText(paramFacesContext, paramUIComponent); } catch (Exception e) { MailUtil mailUtil = new MailUtil(); Object v = FacesUtil.getValue(paramFacesContext, paramUIComponent); String str2 = v.toString(); return mailUtil.sanitizeHtml(str2, true, e); } } }
and MailUtil extract that does the sanitization - I needed it also when we are copying the message content between documents, so it's called in different scenarios, triggering the ACF internally. It's trying to remove the invalid data and if even this would fail, it removes all base64 encoded images.
public String sanitizeHtml(String html, boolean errorCase, Throwable e) {
FacesContext fc = FacesContext.getCurrentInstance();
if (!errorCase) {
try {
html = ((FacesContextEx)fc).filterHtml("acf", html);
return html;
} catch (Exception e1) {
return sanitizeHtml(html, true, e1);
}
} else {
try {
if (e!=null && findCause(e) instanceof ArrayIndexOutOfBoundsException) {
//check for a known problem with base64 followed by cid
String pattern="<img.*[\"'].*base64,cid[^>]*>";
html = html.replaceAll(pattern, "");
html = ((FacesContextEx)fc).filterHtml("acf", html);
return html;
}
} catch (Exception e2) {
//remove all base64 imgs
try {
String pattern="<img.*[\"'].*base64[^>]*>";
html = html.replaceAll(pattern, "");
html = ((FacesContextEx)fc).filterHtml("acf", html);
return html;
} catch (Exception e3) {
OpenLogItem.logError(e3);
}
}
}
return "ERROR loading message content due to invalid structure - please report this issue, including link. Message still can be accessed via Notes client";
}
private static Throwable findCause(Throwable throwable) {
Objects.requireNonNull(throwable);
Throwable rootCause = throwable;
while (rootCause.getCause() != null && rootCause.getCause() != rootCause) {
rootCause = rootCause.getCause();
}
return rootCause;
}
then just register the renderer
<renderer>
<component-family>com.ibm.xsp.InputRichText</component-family>
<renderer-type>com.ibm.xsp.InputRichText.ReadOnly</renderer-type>
<renderer-class>custom.theme.renderkit.ReadOnlyRteRenderer</renderer-class>
</renderer>
We needed it only in read mode as all editable documents would contain the already sanitized version of the data.
The app was not crashing anymore, but another problem was reported.
Unreadable emails after ACF processing
Conflicts between message content and the app
<renderer> <component-family>com.ibm.xsp.InputRichText</component-family> <renderer-type>com.ibm.xsp.InputRichText.ReadOnly</renderer-type> <renderer-class>com.ibm.xsp.renderkit.html_extended.RichTextRenderer</renderer-class> </renderer>
<xp:dojoAttribute name="toolbar"> <xp:this.value><![CDATA[#{javascript:if (pageController.isEditable()) { return '[["Format", "Font", "FontSize"],["Bold", "Italic", "Underline", "Strike", "-", "TextColor", "BGColor", "-", "JustifyLeft", "JustifyCenter", "JustifyRight", "JustifyBlock", "NumberedList", "-", "BulletedList"],["Indent", "Outdent"],["Subscript", "Superscript"],["RemoveFormat", "-", "MenuPaste", "-", "Undo", "Redo", "Find", "LotusSpellChecker", "-", "IbmImage", "Table", "Link", "Flash", "-", "PageBreak", "HorizontalRule", "SpecialChar", "Blockquote", "Smiley", "ShowBlocks"],["Maximize", "Source"]]' } return '[["Maximize"]]'}]]></xp:this.value> </xp:dojoAttribute>
Content not being uploaded after switching to edit mode via partial refresh
<xp:scriptBlock id="scriptBlock3">
<xp:this.value><![CDATA[
try {
if(XSP) {
if (XSP.querySubmitListeners) {
var listeners = XSP.querySubmitListeners;
var listenerId = '#{javascript:getComponent(compositeData.inputId).getClientId(facesContext)}_rteSubmit'.replaceAll(':','_');
for(var i=0;i<listeners.length;i++) {
if (listenerId == listeners[i].scriptId) {
//remove the listener and return as there can be only one
listeners.splice(i,1);
break;
}
}
}
}
} catch(e) {
console.error(e);
}
]]></xp:this.value>
</xp:scriptBlock>
Comments
Post a Comment