Skip to content

Commit

Permalink
Fix #10251: 13.0.6 FileDownload allow disabling of no-store (#11349)
Browse files Browse the repository at this point in the history
  • Loading branch information
melloware committed Jan 30, 2024
1 parent 569914e commit 4edcbd6
Show file tree
Hide file tree
Showing 5 changed files with 49 additions and 9 deletions.
5 changes: 5 additions & 0 deletions docs/13_0_0/components/filedownload.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ stream the binary data. FileDownload presents an easier way to do the same.
| value | null | StreamedContent | A streamed content instance.
| contentDisposition | attachment | String | Specifies display mode on non-AJAX downloads.
| monitorKey | null | String | Optional key to support monitoring multiple filedownloads on same page.
| store | false | Boolean | Controls the `no-store` attribute on the cache control header. Default is to include `no-store`.

## Getting started with FileDownload
A user command action is required to trigger the file-download process.
Expand Down Expand Up @@ -73,6 +74,10 @@ On regular (non-AJAX) downloads, by default, content is displayed as an `attachm
another alternative is the `inline` mode, in this case browser will try to open the file internally without a prompt.
Note that content disposition is not part of the http standard, although it is widely implemented.

## Browser Caching
Most of the time you want to leave store="false" which is the default to not cache the results. However, in the case of PDF Downloads there have
been issues with some browser like Chrome/Edge. See: https://github.com/primefaces/primefaces/issues/10251

## Monitor Status
When fileDownload is used without AJAX, ajaxStatus cannot apply. Still PrimeFaces provides a feature
to monitor file downloads via client side `monitorDownload(startFunction, endFunction)` method.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -51,15 +51,18 @@ public class FileDownloadActionListener implements ActionListener, StateHolder {

private ValueExpression monitorKey;

private ValueExpression store;

public FileDownloadActionListener() {
ResourceUtils.addComponentResource(FacesContext.getCurrentInstance(), "filedownload/filedownload.js");
}

public FileDownloadActionListener(ValueExpression value, ValueExpression contentDisposition, ValueExpression monitorKey) {
public FileDownloadActionListener(ValueExpression value, ValueExpression contentDisposition, ValueExpression monitorKey, ValueExpression store) {
this();
this.value = value;
this.contentDisposition = contentDisposition;
this.monitorKey = monitorKey;
this.store = store;
}

@Override
Expand Down Expand Up @@ -101,7 +104,9 @@ protected void regularDownload(FacesContext context, StreamedContent content) {
? "/"
: externalContext.getRequestContextPath()); // Always add cookies to context root; see #3108
ResourceUtils.addResponseCookie(context, monitorKeyCookieName, "true", cookieOptions);
ResourceUtils.addNoCacheControl(externalContext);

Boolean store = this.store != null ? (Boolean) this.store.getValue(context.getELContext()) : Boolean.FALSE;
ResourceUtils.addNoCacheControl(externalContext, store);

if (content.getContentLength() != null) {
// we can't use externalContext.setResponseContentLength as our contentLength is a long
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,12 +37,14 @@ public class FileDownloadTagHandler extends TagHandler {
private final TagAttribute value;
private final TagAttribute contentDisposition;
private final TagAttribute monitorKey;
private final TagAttribute store;

public FileDownloadTagHandler(TagConfig tagConfig) {
super(tagConfig);
value = getRequiredAttribute("value");
contentDisposition = getAttribute("contentDisposition");
monitorKey = getAttribute("monitorKey");
store = getAttribute("store");
}

@Override
Expand All @@ -54,15 +56,19 @@ public void apply(FaceletContext faceletContext, UIComponent parent) throws IOEx
ValueExpression valueVE = value.getValueExpression(faceletContext, Object.class);
ValueExpression contentDispositionVE = null;
ValueExpression monitorKeyVE = null;
ValueExpression storeVE = null;

if (contentDisposition != null) {
contentDispositionVE = contentDisposition.getValueExpression(faceletContext, String.class);
}
if (monitorKey != null) {
monitorKeyVE = monitorKey.getValueExpression(faceletContext, String.class);
}
if (store != null) {
storeVE = store.getValueExpression(faceletContext, Boolean.class);
}

ActionSource actionSource = (ActionSource) parent;
actionSource.addActionListener(new FileDownloadActionListener(valueVE, contentDispositionVE, monitorKeyVE));
actionSource.addActionListener(new FileDownloadActionListener(valueVE, contentDispositionVE, monitorKeyVE, storeVE));
}
}
18 changes: 17 additions & 1 deletion primefaces/src/main/java/org/primefaces/util/ResourceUtils.java
Original file line number Diff line number Diff line change
Expand Up @@ -162,7 +162,23 @@ public static byte[] toByteArray(InputStream is) {
* @see <a href="https://github.com/primefaces/primefaces/issues/6359">FileDownload: configure Cache-Control</a>
*/
public static void addNoCacheControl(ExternalContext externalContext) {
externalContext.setResponseHeader("Cache-Control", "no-cache, no-store, must-revalidate");
addNoCacheControl(externalContext, false);
}

/**
* Adds no cache pragma to the response to ensure it is not cached. Dynamic downloads should always add this
* to prevent caching and for GDPR.
*
* @param externalContext the ExternalContext we add the pragma to
* @param store used to add no-store or exclude it if false
* @see <a href="https://github.com/primefaces/primefaces/issues/6359">FileDownload: configure Cache-Control</a>
*/
public static void addNoCacheControl(ExternalContext externalContext, boolean store) {
String cacheControl = "no-cache, no-store, must-revalidate";
if (store) {
cacheControl = "no-cache, must-revalidate";
}
externalContext.setResponseHeader("Cache-Control", cacheControl);
externalContext.setResponseHeader("Pragma", "no-cache");
externalContext.setResponseHeader("Expires", "0");
}
Expand Down
18 changes: 13 additions & 5 deletions primefaces/src/main/resources/META-INF/primefaces-p.taglib.xml
Original file line number Diff line number Diff line change
Expand Up @@ -229,31 +229,39 @@
</tag>

<tag>
<description>The legacy way to present dynamic binary data to the client is to write a servlet or a filter and stream the binary data.
FileDownload presents an easier way to do the same.
<description><![CDATA[The legacy way to present dynamic binary data to the client is to write a servlet or a filter and stream the binary data.
FileDownload presents an easier way to do the same.]]>
</description>
<tag-name>fileDownload</tag-name>
<handler-class>
org.primefaces.component.filedownload.FileDownloadTagHandler
</handler-class>
<attribute>
<description>A streamed content instance.</description>
<description><![CDATA[A streamed content instance.]]></description>
<name>value</name>
<required>true</required>
<type>java.lang.Object</type>
</attribute>
<attribute>
<description>Specifies display mode (non-ajax), valid values are "attachment" (default) and "inline".</description>
<description><![CDATA[Specifies display mode (non-ajax), valid values are "attachment" (default) and "inline".]]></description>
<name>contentDisposition</name>
<required>false</required>
<type>java.lang.String</type>
</attribute>
<attribute>
<description>Defines setting cookie key for monitorDownload method on client side.</description>
<description><![CDATA[Defines setting cookie key for monitorDownload method on client side.]]></description>
<name>monitorKey</name>
<required>false</required>
<type>java.lang.String</type>
</attribute>
<attribute>
<description>
<![CDATA[Controls the 'no-store' attribute on the cache control header. Default false is to include 'no-store'.]]>
</description>
<name>store</name>
<required>false</required>
<type>java.lang.Boolean</type>
</attribute>
</tag>

<tag>
Expand Down

0 comments on commit 4edcbd6

Please sign in to comment.