Listen for Document And Media view/download events - audit

I want my code to be called everytime someone views or downloads anything in Document And Media:
View:
Download:
The content URLs of the view and download (to which the HTTP response is an actual preview image or PDF itself being transfered) are respectively:
http://localhost:8080/documents/20143/0/invoice_ABC_2017.10.27.pdf/c44fd479-331b-f393-7879-973c5cecf086?version=1.0&previewFileIndex=1
http://localhost:8080/documents/20143/0/invoice_ABC_2017.10.27.pdf/c44fd479-331b-f393-7879-973c5cecf086?download=true
The responses to both requests seems to be built by WebServerServlet.sendFile, a part of Liferay which is unfortunately not an OSGi module.
My first instinct would have been to implement ModelListener, but it only has methods for creation/update/deletion events, nothing for read events.
How to intercept these events in Liferay? (7 EE DXP)

Model listeners are connected to the CRUD operation that can happen on an entity.
You could attach your self to the download action. Have a look here https://dev.liferay.com/develop/tutorials/-/knowledge_base/7-0/converting-strutsactionwrappers-to-mvccommands

Preview
Preview (in the sense of the preview page being displayed by any user) can be intercepted by deploying a component that takes the place of the MVCRenderCommand.class service. To do that, copy Liferay's ViewFileEntryMVCRenderCommand.java and add your code in the render method:
#Component(
property = {
"service.ranking:Integer=100",
"javax.portlet.name=" + DLPortletKeys.DOCUMENT_LIBRARY,
"javax.portlet.name=" + DLPortletKeys.DOCUMENT_LIBRARY_ADMIN,
"javax.portlet.name=" + DLPortletKeys.MEDIA_GALLERY_DISPLAY,
"mvc.command.name=/document_library/view_file_entry"
},
service = MVCRenderCommand.class
)
public class MyViewFileEntryMVCRenderCommand implements MVCRenderCommand {
#Override
public String render(
RenderRequest renderRequest, RenderResponse renderResponse)
throws PortletException {
DoMyAuditThing();
[...]
}
[...]
}
Download
Download (in the sense of a Document and Media being actually downloaded) can be intercepted by creating a Servlet Filter (copied from the Liferay plugin samples) with this liferay-hook.xml configuration:
(UPDATE: Just after writing this code I realized that there is now a better way to write Servlet Filters)
<hook>
<servlet-filter>
<servlet-filter-name>Sample Filter</servlet-filter-name>
<servlet-filter-impl>com.liferay.sampleservletfilter.hook.filter.SampleFilter</servlet-filter-impl>
<init-param>
<param-name>hello</param-name>
<param-value>world</param-value>
</init-param>
</servlet-filter>
<servlet-filter-mapping>
<servlet-filter-name>Sample Filter</servlet-filter-name>
<before-filter>SSO Open SSO Filter</before-filter>
<url-pattern>/documents/*</url-pattern>
<dispatcher>REQUEST</dispatcher>
<dispatcher>FORWARD</dispatcher>
</servlet-filter-mapping>
</hook>
Note the <url-pattern>/documents/*</url-pattern> part.
The Filter class:
public class SampleFilter implements Filter {
#Override
public void doFilter(
ServletRequest servletRequest, ServletResponse servletResponse,
FilterChain filterChain)
throws IOException, ServletException {
String uri = (String)servletRequest.getAttribute(
WebKeys.INVOKER_FILTER_URI);
// Extract information
String[] tokens = uri.split("/");
if(tokens.length < 6) {
System.out.println("Failed to parse download URI (Too few slashes): " + uri);
filterChain.doFilter(servletRequest, servletResponse);
return;
}
long groupId;
try {
groupId = Long.parseLong(tokens[2]);
}
catch(NumberFormatException e) {
System.out.println("Failed to parse download URI (Group not a number): " + uri);
filterChain.doFilter(servletRequest, servletResponse);
return;
}
String uuid = tokens[5];
System.out.println("group:" + groupId + " uuid:" + uuid);
DLFileEntry fileEntry = DLFileEntryLocalServiceUtil.fetchDLFileEntryByUuidAndGroupId(uuid, groupId);
// Send it to your audit
[...]
filterChain.doFilter(servletRequest, servletResponse);
}
}
A problem is that it seems to also catch unnecessary events when showing the Document and Media page... I investigate.

You could implement the Service Wrapper and in particular the getFile method. This method it’s called when the user request the download of the file.

Related

JSF2.0: Show certain pdf page on load

I'd like to open a PDF in a new Page from JSF2, and display a certain page in this pdf on load. I have a kind of TOC in my jsf page, and want to jump from there to the page in the PDF directly.
What I know (this is not, what I need, just an example of giving adobe reader and other pdf readers the page I want to jump to):
Something like this will open the page (chose something from the internet):
https://www.cdc.gov/diabetes/pdfs/data/statistics/national-diabetes-statistics-report.pdf#page=10
The #page=10 makes the pdf plugin of the browser display page 10.
Requirements for selecting the PDF:
PDF is dynamically downloaded from a webservice according to an ID that must only reside in the ManagedBeans, since it's secret, and should not be passed to others (like Session ID...) (below given anser by me passes the ID in the GET-Parameter, which should not be done)
PDF should not reside in the Filesystem, sinc I don't want the handling of temporary files (below given answer by me actually utilizes PDFs on FS, with stream only it does not work)
Now my real problem: I have to change the URL beeing displayed/used in JSF, but can't use the normal way with and includeViewParams, because this will insert a "?", and not a "#" in the URL.
Also, I have a backing bean, that gets the content of the PDF from a backend service, based on some other parameters I'm giving, so a solution with would be cool, but I'm aware that this is probably not possible...
Does anyone have an idea, how to solve this?
I didn't include any code, since it doesn't work anyways, and I probably need a completely new way to solve this anyways...
Turns out, Primefaces has this already implemented (although the implementation has it's restrictions):
<p:media player="pdf" value="#{viewerBean.media}" width="100%" height="100%">
<f:param name="#page" value="#{viewerBean.pageNumber}"/>
<f:param name="toolbar" value="1"/>
<!--<f:param name="search" value="#{viewerBean.queryText}"/>-->
</p:media>
https://www.primefaces.org/showcase/ui/multimedia/media.xhtml
Restriction: Can't read from a stream, at least not very stable. Save your energy, and write a stream to a temp file, and set this filename dynamically. Not sure, whether this is complete, but you should get the idea:
import javax.faces.bean.ManagedProperty;
import javax.faces.bean.RequestScoped;
import java.io.*;
import javax.annotation.PostConstruct;
import java.nio.file.Files;
import java.nio.file.Paths;
#ManagedBean
#RequestScoped
public class ViewerBean implements Serializable {
#ManagedProperty(value = "#{param.page}")
private String pageNumber;
private File media;
#PostConstruct
public void init() {
try {
media = Files.createTempFile("car", ".pdf").toFile();
try (FileOutputStream outputStream = new FileOutputStream(media)) {
IOUtils.copy(getStreamedContent().getStream(), outputStream);
}
} catch (IOException e) {
LOGGER.error(e);
throw new RuntimeException("Error creating temp file", e);
}
}
public StreamedContent getMedia() {
try {
return new DefaultStreamedContent(new FileInputStream(media), "application/pdf");
} catch (FileNotFoundException e) {
String message = "Error reading file " + media.getAbsolutePath();
LOGGER.error(message, e);
throw new RuntimeException(message, e);
}
}
}
If the pagename is not needed, you could use this:
http://balusc.omnifaces.org/2006/05/pdf-handling.html
Maybe if you can utilize outputLink for this you'll be lucky, but I ran out of time to test this option.
Found the (THE) solution; above answher mentions , but this cannot cope with #ViewScope beans, and sends many requests to the underlying bean for reading only one InputStream. I found this not acceptable for load reasons.
So here we go:
Create JSF page with <f:event type="preRenderView" listener="#{documentDownloadBean.writeIntpuStreamToResponseOutputStream}"/>
Put neccessary data for dynamic retrieval of the PDF into flash scope
redirect to above JSF page like so: return "document_search/view_pdf.xhtml?faces-redirect=true#page=" + page;
#ManagedBean
#ViewScoped
public class DocumentDownloadBean implements Serializable {
#ManagedProperty(value = "#{documentSearchBean}")
private DocumentSearchBean documentSearchBean;
public String activeDocumentToFlashScope(String page) {
Document document = documentSearchBean.getSelectedDocument();
FacesContext.getCurrentInstance().getExternalContext().getFlash().put("document", document);
// everything preapared now, redirect to viewing JSF page, with page=xxx parameter in URL, which will be evaluated by adobe pdf reader (and other readers, too)
return "document_search/view_pdf.xhtml?faces-redirect=true#page=" + page;
}
public void download() {
Document document = (Document) FacesContext.getCurrentInstance().getExternalContext().getFlash().get("document");
InputStream inputStream = getInputstreamFromBackingWebserviceSomehow(document);
FacesUtils.writeToResponseStream(FacesContext.getCurrentInstance().getExternalContext(), inputStream, document.getFileName());
}
}
Calling JSF Page:
<p:commandLink id="outputText" action="#{documentDownloadBean.activeDocumentToFlashScope(selectedDocument, page)}"
target="_blank" ajax="false">
<h:outputText value="View PDF"/>
</p:commandLink>

Can't upload files in spring boot

I've been struggling with this for the past 3 days now, I keep getting the following exception when I try upload a file in my spring boot project.
org.springframework.web.multipart.support.MissingServletRequestPartException: Required request part 'file' is not present
I'm not sure if it makes a differance but I am deploying my application as a war onto weblogic,
here is my controller
#PostMapping
public AttachmentDto createAttachment(#RequestParam(value = "file") MultipartFile file) {
logger.info("createAttachment - {}", file.getOriginalFilename());
AttachmentDto attachmentDto = null;
try {
attachmentDto = attachmentService.createAttachment(new AttachmentDto(file, 1088708753L));
} catch (IOException e) {
e.printStackTrace();
}
return attachmentDto;
}
multi part beans I can see in spring boot actuator
payload seen in chrome
Name attribute is required for #RequestParm 'file'
<input type="file" class="file" name="file"/>
You can try use #RequestPart, because it uses HttpMessageConverter, that takes into consideration the 'Content-Type' header of the request part.
Note that #RequestParam annotation can also be used to associate the part of a "multipart/form-data" request with a method argument supporting the same method argument types. The main difference is that when the method argument is not a String, #RequestParam relies on type conversion via a registered Converter or PropertyEditor while #RequestPart relies on HttpMessageConverters taking into consideration the 'Content-Type' header of the request part. #RequestParam is likely to be used with name-value form fields while #RequestPart is likely to be used with parts containing more complex content (e.g. JSON, XML).
Spring Documentation
Code:
#PostMapping(consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
public AttachmentDto createAttachment(#RequestPart("file") MultipartFile file) {
logger.info("Attachment - {}", file.getOriginalFilename());
try {
return attachmentService.createAttachment(new AttachmentDto(file, 1088708753L));
} catch (final IOException e) {
logger.e("Error creating attachment", e);
}
return null;
}
You are using multi part to send files so there is nothing much configuration to do to get desired result.
I m having the same requirement and my code just run fine :
#RestController
#RequestMapping("/api/v2")
public class DocumentController {
private static String bucketName = "pharmerz-chat";
// private static String keyName = "Pharmerz"+ UUID.randomUUID();
#RequestMapping(value = "/upload", method = RequestMethod.POST, consumes = MediaType.MULTIPART_FORM_DATA)
public URL uploadFileHandler(#RequestParam("name") String name,
#RequestParam("file") MultipartFile file) throws IOException {
/******* Printing all the possible parameter from #RequestParam *************/
System.out.println("*****************************");
System.out.println("file.getOriginalFilename() " + file.getOriginalFilename());
System.out.println("file.getContentType()" + file.getContentType());
System.out.println("file.getInputStream() " + file.getInputStream());
System.out.println("file.toString() " + file.toString());
System.out.println("file.getSize() " + file.getSize());
System.out.println("name " + name);
System.out.println("file.getBytes() " + file.getBytes());
System.out.println("file.hashCode() " + file.hashCode());
System.out.println("file.getClass() " + file.getClass());
System.out.println("file.isEmpty() " + file.isEmpty());
/**
BUSINESS LOGIC
Write code to upload file where you want
*****/
return "File uploaded";
}
None of the above solutions worked for me, but when I digged deeper i found that spring security was the main culprit. Even if i was sending the CSRF token, I repeatedly faced the issue POST not supported. I came to know that i was receiving forbidden 403 when i inspected using developer tools in google chrome and saw the status code in the network tab. I added the mapping to ignoredCsrfMapping in Spring Security configuration and then it worked absolutely without any other flaw. Don't know why i was not allowed to post multipart data by security. Some of the mandatory setting that needs to be stated in application.properties file are as follows:
spring.servlet.multipart.max-file-size=10MB
spring.servlet.multipart.max-request-size=10MB
spring.http.multipart.max-file-size=10MB
spring.http.multipart.max-request-size=10MB
spring.http.multipart.enabled=true

Overwrite the LoginWidget and Set DestinationPageUrl dynamically in Sitefinity

I want to add some custom code during the login function, in particular i want to redirect the user after login to the previous page.
For example: i'm on page A , i want to download something from this page, but i'm not authorized. Then pops a popup with link to the login page. After successful login i'm back on page A.
For this purpose i want to overwrite the LoginWidged and to set value to"this.DestinationPageUrl" dynamically.
I read about similar issues here and here, but there isn't an example how to overwrite this LoginWidget class.
I create CustomLoginControl.cs file in my project and register as a new custom control, but after rendering it on the page, it didn't work. Login button does not make nothing. I'm not sure what exactly have to do and which of methods have to overwrite.
namespace SitefinityWebApp.UserControls
{
public class CustomLoginControl : Telerik.Sitefinity.Web.UI.PublicControls.LoginWidget
{
protected override void Render(System.Web.UI.HtmlTextWriter writer)
{
this.DestinationPageUrl = "http://previousPage.com";
base.Render(writer);
}
}
}
Can you give me an example how to overwrite this class to work properly.
Version: Sitefinity 5.0, Claims-based authentication
I've done something similar but instead of overriding the login control you can subscribe and capture the UnauthorizedAccess event, send the user to your login page with your redirect page as a query string parameter. You'll need to add a Global.asax / Global.asax.cs file to your project, then add this to the Application_Start function:
protected void Application_Start(object sender, EventArgs e)
{
Bootstrapper.Initialized += BootstrapperInitialized;
}
Then add these two functions:
private void BootstrapperInitialized(object sender, ExecutedEventArgs e)
{
if (e.CommandName == "Bootstrapped")
{
EventHub.Subscribe<IUnauthorizedPageAccessEvent>(OnUnauthorizedAccess);
}
}
private void OnUnauthorizedAccess(IUnauthorizedPageAccessEvent unauthorizedEvent)
{
var manager = ConfigManager.GetManager();
string loginPage = manager.GetSection<ProjectConfig>().DefaultSite.FrontEndLoginPageUrl;
var redirectParam = unauthorizedEvent.RedirectUrl.Replace(string.Format("{0}?ReturnUrl=", loginPage), string.Empty);
var escaped = Uri.EscapeDataString(redirectParam);
unauthorizedEvent.HttpContext.Response.Redirect(string.Format("{0}?ReturnUrl={1}", loginPage, escaped));
}
You will also need to set your default front end login page in the settings under Administration -> Settings -> Advanced -> Project -> DefaultSite and the FrontEndLoginPageUrl setting.
This works for me on a 6.3 site, not sure if this is available in Sitefinity 5 or not.

Unable to show PDF in p:media generated from streamed content in Primefaces

I'm trying to show inline PDF which is opened in new browser window. I have following scenario:
In some ActionListen which is called by ajax I generate PDF content, put data in session, and send Javascript to be executed (window.open to open new page to show PDF)
On opened page I just have p:media tag inside h:body with value pointing to StreamedContent:
Now, on that page my PDF is not generated. In log I can see these two lines:
org.primefaces.application.PrimeResourceHandler handleResourceRequest
SEVERE: Error in streaming dynamic resource. Expression cannot be null
I started to debug and find out a few things.
First, I added breakpoint to #PostConstruct method of my RequestScoped bean. What is interesting is that breakpoint is reached twice, and to my big surprise after that PDF is shown perfectly?!
After some debugging through PrimeResourceHandler I figure out that in some cases ValueExpression is not calculated, in fact it throws NullPointerException, and again while debugging I saw that two requests are sent, and second request fails because dynamicContentId is removed in first request, and second call to handleResourceRequest doesn't have sense.
Through Firebug I can see two requests, first which is good with PDF data, and second which is also with content-type application/pdf but empty, with size 0.
xhtml page:
<html>
<h:head></h:head>
<h:body>
<p:media value="#{reportBean.streamedContent}" player="pdf" width="500" height="500"/>
</h:body>
</html>
backing bean:
#RequestScoped
public class StampaListeBackingBean implements Serializable {
private static final long serialVersionUID = 1L;
private StreamedContent streamedContent;
#PostConstruct
public void init() {
Map<String, Object> session = FacesContext.getCurrentInstance().getExternalContext().getSessionMap();
byte[] b = (byte[]) session.get("reportBytes");
if (b != null) {
streamedContent = new DefaultStreamedContent(new ByteArrayInputStream(b), "application/pdf");
}
}
public StreamedContent getStreamedContent() {
if (FacesContext.getCurrentInstance().getRenderResponse()) {
return new DefaultStreamedContent();
} else {
return streamedContent;
}
}
public void setStreamedContent(StreamedContent streamedContent) {
this.streamedContent = streamedContent;
}
}
I need to understand why two requests are sent on page with p:media tag, and to figure out how to make this work. Backing bean is request scoped, it creates StreamedContent in #PostConstruct method, and has getter and setter for that field. Primefaces version is 3.4.2, with Mojarra 2.1.14.
ADDED:
It is easy to reproduce my problem. If code in init method is replaced with following:
FileInputStream fis = new FileInputStream(new File("C:\\samplexxx.pdf"));
streamedContent = new DefaultStreamedContent(fis, "application/pdf");
problem can be reproduced.
I can reproduce your problem. It indeed doesn't work in Firefox (nor in IE9, but it works in Chrome). PrimeFaces lead Cagatay has also mentioned that several times.
I'm not sure if this is a bug in the PrimeFaces resource handler or in the browser. I'll leave it in the middle.
In the meanwhile, your best bet is a simple web servlet for the job. Just create this class:
#WebServlet("/report.pdf")
public class PdfReportServlet extends HttpServlet {
#Override
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
byte[] content = (byte[]) request.getSession().getAttribute("reportBytes");
response.setContentType("application/pdf");
response.setContentLength(content.length);
response.getOutputStream().write(content);
}
}
And invoke it as follows:
<p:media value="/report.pdf" ... />
That's it. No XML config necessary. It works for me in all browsers. Depending on the functional requirements, you may want to further finetune response headers related to browser caching.
It is not a browser or primefaces problem, just a funny getter problem.
The getter is called twice by p:media (or if you refresh page than more times), but only the 1st call gets the correct data. StreamedContent encapsulates an InputStream, which has the property that it will give no bytes if the stream is at the end of the file. First time it is read to its end (data is ok), but every next call will get no data. :)
javadoc of inputStream.read():
If no byte is available because the stream is at the end of the file, the value -1 is returned; otherwise, at least one byte is read and stored into b.
Solution:
private StreamedContent streamedContent;
private InputStream stream;
public void somewhere(){
byte[] b = ...
stream = new ByteArrayInputStream( b );
stream.mark(0); //remember to this position!
streamedContent = new DefaultStreamedContent(stream, "application/pdf");
}
public StreamedContent getStreamedContent() {
if (streamedContent != null)
streamedContent.getStream().reset(); //reset stream to the start position!
return streamedContent;
}
I hope my little contribution can help anyone who can't display pdf preview in Firefox. I was using Primefaces 6 + Spring and I had the same problem but maybe not due the same reason. Indeed, I tried the proposed solution by Balus C. It helped me to display the pdf in Chrome and IE11 but it still was not working in Firefox 52.
I noticed an error in the Firefox console: Load denied by X-Frame-Options: http://localhost:8080/myapp/ does not permit framing
In my case, it was because spring-security configuration and the solution was edit spring-context.xml in this way:
<sec:http ...>
...
<sec:headers>
<sec:frame-options policy="SAMEORIGIN" />
</sec:headers>
...
</sec:http>

Returning binary content from a JPF action with Weblogic Portal 10.2

One of the actions of my JPF controller builds up a PDF file and I would like to return this file to the user so that he can download it.
Is it possible to do that or am I forced to write the file somewhere and have my action forward a link to this file? Note that I would like to avoid that as much as possible for security reasons and because I have no way to know when the user has downloaded the file so that I can delete it.
I've tried to access the HttpServletResponse but nothing happens:
getResponse().setContentLength(file.getSize());
getResponse().setContentType(file.getMimeType());
getResponse().setHeader("Content-Disposition", "attachment;filename=\"" + file.getTitle() + "\"");
getResponse().getOutputStream().write(file.getContent());
getResponse().flushBuffer();
We have something similar, except returning images instead of a PDF; should be a similar solution, though, I'm guessing.
On a JSP, we have an IMG tag, where the src is set to:
<c:url value="/path/getImage.do?imageId=${imageID}" />
(I'm not showing everything, because I'm trying to simplify.) In your case, maybe it would be a link, where the href is done in a similar way.
That getImage.do maps to our JPF controller, obviously. Here's the code from the JPF getImage() method, which is the part you're trying to work on:
#Jpf.Action(forwards = {
#Jpf.Forward(name = FWD_SUCCESS, navigateTo = Jpf.NavigateTo.currentPage),
#Jpf.Forward(name = FWD_FAILURE, navigateTo = Jpf.NavigateTo.currentPage) })
public Forward getImage(final FormType pForm) throws Exception {
final HttpServletRequest lRequest = getRequest();
final HttpServletResponse lResponse = getResponse();
final HttpSession lHttpSession = getSession();
final String imageIdParam = lRequest.getParameter("imageId");
final long header = lRequest.getDateHeader("If-Modified-Since");
final long current = System.currentTimeMillis();
if (header > 0 && current - header < MAX_AGE_IN_SECS * 1000) {
lResponse.setStatus(HttpServletResponse.SC_NOT_MODIFIED);
return null;
}
try {
if (imageIdParam == null) {
throw new IllegalArgumentException("imageId is null.");
}
// Call to EJB, which is retrieving the image from
// a separate back-end system
final ImageType image = getImage(lHttpSession, Long
.parseLong(imageIdParam));
if (image == null) {
lResponse.sendError(404, IMAGE_DOES_NOT_EXIST);
return null;
}
lResponse.setContentType(image.getType());
lResponse.addDateHeader("Last-Modified", current);
// public: Allows authenticated responses to be cached.
lResponse.setHeader("Cache-Control", "max-age=" + MAX_AGE_IN_SECS
+ ", public");
lResponse.setHeader("Expires", null);
lResponse.setHeader("Pragma", null);
lResponse.getOutputStream().write(image.getContent());
} catch (final IllegalArgumentException e) {
LogHelper.error(this.getClass(), "Illegal argument.", e);
lResponse.sendError(404, IMAGE_DOES_NOT_EXIST);
} catch (final Exception e) {
LogHelper.error(this.getClass(), "General exception.", e);
lResponse.sendError(500);
}
return null;
}
I've actually removed very little from this method, because there's very little in there that I need to hide from prying eyes--the code is pretty generic, concerned with images, not with business logic. (I changed some of the data type names, but no big deal.)