diff --git a/api/src/main/java/edu/cornell/mannlib/vedit/controller/OperationController.java b/api/src/main/java/edu/cornell/mannlib/vedit/controller/OperationController.java index db98ba2a97..044b66adb4 100644 --- a/api/src/main/java/edu/cornell/mannlib/vedit/controller/OperationController.java +++ b/api/src/main/java/edu/cornell/mannlib/vedit/controller/OperationController.java @@ -48,6 +48,9 @@ public class OperationController extends BaseEditController { private static final Log log = LogFactory.getLog(OperationController.class.getName()); + private static final List ignoreReferers = Arrays.asList("/siteStyle", "/uploadImages"); + + public void doPost (HttpServletRequest request, HttpServletResponse response) { @@ -81,6 +84,10 @@ public void doPost (HttpServletRequest request, HttpServletResponse response) { // if we're canceling, we don't need to do anything if (request.getParameter("_cancel") != null){ String referer = epo.getReferer(); + boolean ignoreReferer = ignoreReferers.stream().anyMatch(referer::contains); + if (ignoreReferer) { + referer = null; + } if (referer == null) { try { response.sendRedirect(defaultLandingPage); diff --git a/api/src/main/java/edu/cornell/mannlib/vitro/webapp/beans/ApplicationBean.java b/api/src/main/java/edu/cornell/mannlib/vitro/webapp/beans/ApplicationBean.java index 563b1c9b44..d61d0ef038 100644 --- a/api/src/main/java/edu/cornell/mannlib/vitro/webapp/beans/ApplicationBean.java +++ b/api/src/main/java/edu/cornell/mannlib/vitro/webapp/beans/ApplicationBean.java @@ -49,6 +49,9 @@ public class ApplicationBean { private String copyrightAnchor; private String themeDir; + private String customCssPath; + + public String toString() { String output = "Application Bean Contents:\n"; output += " initialized from DB: [" + initialized + "]\n"; @@ -116,6 +119,10 @@ public void setThemeDir(String string_val) { themeDir = string_val; } + public void setCustomCssPath(String string_val) { + customCssPath = string_val; + } + /*************************** GET functions ****************************/ public String getSessionIdStr() { @@ -166,6 +173,10 @@ public String getCopyrightAnchor() { return copyrightAnchor; } + public String getCustomCssPath() { + return customCssPath; + } + // TODO deprecate or remove the following three legacy methods? public String getRootBreadCrumbURL() { diff --git a/api/src/main/java/edu/cornell/mannlib/vitro/webapp/controller/edit/ApplicationBeanRetryController.java b/api/src/main/java/edu/cornell/mannlib/vitro/webapp/controller/edit/ApplicationBeanRetryController.java index 936279e83b..b1e60e9c17 100644 --- a/api/src/main/java/edu/cornell/mannlib/vitro/webapp/controller/edit/ApplicationBeanRetryController.java +++ b/api/src/main/java/edu/cornell/mannlib/vitro/webapp/controller/edit/ApplicationBeanRetryController.java @@ -11,6 +11,7 @@ import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; +import edu.cornell.mannlib.vitro.webapp.controller.freemarker.SiteStyleController; import edu.cornell.mannlib.vitro.webapp.utils.JSPPageHandler; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; @@ -89,6 +90,7 @@ public void doPost (HttpServletRequest req, HttpServletResponse response) { request.setAttribute("title","Site Information"); request.setAttribute("_action",action); request.setAttribute("unqualifiedClassName","ApplicationBean"); + request.setAttribute("siteStyleUrl", SiteStyleController.URL_HERE); setRequestAttributes(request,epo); try { diff --git a/api/src/main/java/edu/cornell/mannlib/vitro/webapp/controller/freemarker/FreemarkerHttpServlet.java b/api/src/main/java/edu/cornell/mannlib/vitro/webapp/controller/freemarker/FreemarkerHttpServlet.java index 13b3c56758..8625df56aa 100644 --- a/api/src/main/java/edu/cornell/mannlib/vitro/webapp/controller/freemarker/FreemarkerHttpServlet.java +++ b/api/src/main/java/edu/cornell/mannlib/vitro/webapp/controller/freemarker/FreemarkerHttpServlet.java @@ -473,6 +473,7 @@ protected Map getPageTemplateValues(VitroRequest vreq) { map.put("scripts", new Tags().wrap()); map.put("headScripts", new Tags().wrap()); map.put("metaTags", new Tags().wrap()); + map.put("customCssPath", vreq.getAppBean().getCustomCssPath()); return map; } diff --git a/api/src/main/java/edu/cornell/mannlib/vitro/webapp/controller/freemarker/ReferrerHelper.java b/api/src/main/java/edu/cornell/mannlib/vitro/webapp/controller/freemarker/ReferrerHelper.java new file mode 100644 index 0000000000..7b1db322aa --- /dev/null +++ b/api/src/main/java/edu/cornell/mannlib/vitro/webapp/controller/freemarker/ReferrerHelper.java @@ -0,0 +1,48 @@ +/* $This file is distributed under the terms of the license in LICENSE$ */ + +package edu.cornell.mannlib.vitro.webapp.controller.freemarker; + +import edu.cornell.mannlib.vitro.webapp.controller.VitroRequest; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +/** + * Handle the storing and retreiving referrer url. + */ +public class ReferrerHelper { + private static final Log log = LogFactory.getLog(ReferrerHelper.class); + + private final String referrerId; + private final String defaultBack; + + ReferrerHelper() { + this.referrerId = "referrer.helper"; + this.defaultBack = null; + } + + ReferrerHelper(String referrerId, String defaultBack) { + this.referrerId = "referrer." + referrerId; + this.defaultBack = defaultBack; + } + + public void captureReferringUrl(VitroRequest vreq) { + String referrer = vreq.getHeader("Referer"); + if (referrer == null) { + vreq.getSession().removeAttribute(this.referrerId); + } else { + vreq.getSession().setAttribute(this.referrerId, referrer); + } + } + + public String getExitUrl(VitroRequest vreq) { + String referrer = (String) vreq.getSession().getAttribute( + referrerId); + + if (referrer != null) { + return referrer; + } + + return defaultBack; + } + +} diff --git a/api/src/main/java/edu/cornell/mannlib/vitro/webapp/controller/freemarker/SiteStyleController.java b/api/src/main/java/edu/cornell/mannlib/vitro/webapp/controller/freemarker/SiteStyleController.java new file mode 100644 index 0000000000..13d548d6ca --- /dev/null +++ b/api/src/main/java/edu/cornell/mannlib/vitro/webapp/controller/freemarker/SiteStyleController.java @@ -0,0 +1,200 @@ +/* $This file is distributed under the terms of the license in LICENSE$ */ + +package edu.cornell.mannlib.vitro.webapp.controller.freemarker; + +import javax.servlet.ServletException; +import javax.servlet.annotation.WebServlet; + + +import edu.cornell.mannlib.vitro.webapp.beans.ApplicationBean; +import edu.cornell.mannlib.vitro.webapp.dao.ApplicationDao; +import edu.cornell.mannlib.vitro.webapp.dao.WebappDaoFactory; +import org.apache.tika.Tika; + +import edu.cornell.mannlib.vitro.webapp.auth.permissions.SimplePermission; +import edu.cornell.mannlib.vitro.webapp.filestorage.UploadedFileHelper; +import edu.cornell.mannlib.vitro.webapp.filestorage.model.FileInfo; +import org.apache.commons.fileupload.FileItem; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +import edu.cornell.mannlib.vitro.webapp.controller.freemarker.UrlBuilder.ParamMap; +import edu.cornell.mannlib.vitro.webapp.application.ApplicationUtils; + +import edu.cornell.mannlib.vitro.webapp.auth.requestedAction.AuthorizationRequest; + +import edu.cornell.mannlib.vitro.webapp.controller.VitroRequest; +import edu.cornell.mannlib.vitro.webapp.controller.freemarker.FileUploadController.FileUploadException; + +import edu.cornell.mannlib.vitro.webapp.controller.freemarker.responsevalues.ResponseValues; +import edu.cornell.mannlib.vitro.webapp.controller.freemarker.responsevalues.TemplateResponseValues; + +import edu.cornell.mannlib.vitro.webapp.modules.fileStorage.FileStorage; + + +import java.io.IOException; +import java.io.InputStream; +import java.util.Objects; + +/** + * Handle adding, replacing or deleting the custom css file. + */ +@WebServlet(name = "SiteStyleController", urlPatterns = { "/siteStyle" }) +public class SiteStyleController extends FreemarkerHttpServlet { + private static final long serialVersionUID = 1L; + private static final Log log = LogFactory + .getLog(SiteStyleController.class); + + /** Limit file size to 6 megabytes. */ + public static final int MAXIMUM_FILE_SIZE = 6 * 1024 * 1024; + + /** The form field of the uploaded file; use as a key to the FileItem map. */ + public static final String PARAMETER_UPLOADED_FILE = "fileUpload"; + + public static final String TEMPLATE = "siteAdmin/siteAdmin-siteStyle.ftl"; + + public static final String URL_HERE = UrlBuilder.getUrl("/siteStyle"); + private static final String PARAMETER_ACTION = "action"; + + public static final String BODY_BACK_LOCATION = "backLocation"; + public static final String BODY_FORM_ACTION_UPLOAD = "actionUpload"; + public static final String BODY_FORM_ACTION_REMOVE = "actionRemove"; + public static final String ACTION_UPLOAD = "upload"; + public static final String ACTION_REMOVE = "remove"; + + + private FileStorage fileStorage; + private ReferrerHelper referrerHelper; + /** + * When initialized, get a reference to the File Storage system. Without + * that, we can do nothing. + */ + @Override + public void init() throws ServletException { + super.init(); + fileStorage = ApplicationUtils.instance().getFileStorage(); + referrerHelper = new ReferrerHelper("siteStyle", "editForm?controller=ApplicationBean"); + } + + /** + * How large an css file will we accept? + */ + @Override + public long maximumMultipartFileSize() { + return MAXIMUM_FILE_SIZE; + } + + /** + * What will we do if there is a problem parsing the request? + */ + @Override + public boolean stashFileSizeException() { + return true; + } + + /** + * The required action depends on what we are trying to do. + */ + @Override + protected AuthorizationRequest requiredActions(VitroRequest vreq) { + return SimplePermission.EDIT_SITE_INFORMATION.ACTION; + } + + /** + * Handle the different actions. If not specified, the default action is to + * show the intro screen. + * @throws FileUploadException + */ + @Override + protected ResponseValues processRequest(VitroRequest vreq) throws FileUploadException { + String action = vreq.getParameter(PARAMETER_ACTION); + + if (Objects.equals(vreq.getMethod(), "POST")) { + + if (action.equals("upload")) { + return uploadCssFile(vreq); + } else if (action.equals("remove")) { + return removeCssFile(vreq); + } else { + this.referrerHelper.captureReferringUrl(vreq); + return showMainStyleEditPage(vreq); + } + } + + this.referrerHelper.captureReferringUrl(vreq); + return showMainStyleEditPage(vreq); + } + + private String getMediaType(FileItem file) { + Tika tika = new Tika(); + InputStream is; + String mediaType = ""; + try { + is = file.getInputStream(); + mediaType = tika.detect(is); + } catch (IOException e) { + log.error(e.getLocalizedMessage()); + } + return mediaType; + } + + private FileItem getUploadedFile(VitroRequest vreq) throws FileUploadException { + if (vreq.getFiles().isEmpty() || vreq.getFiles().get(PARAMETER_UPLOADED_FILE) == null) { + throw new FileUploadController.FileUploadException("Wrong file type uploaded or file is too big."); + } + return vreq.getFiles().get(PARAMETER_UPLOADED_FILE).get(0); + } + + private FileInfo createFile(FileItem file, String storedFileName, UploadedFileHelper fileHelper) + throws FileUploadController.FileUploadException { + FileInfo fileInfo = null; + try { + fileInfo = fileHelper.createFile(storedFileName, getMediaType(file), file.getInputStream()); + } catch (Exception e) { + log.error(e.getLocalizedMessage()); + throw new FileUploadController.FileUploadException(e.getLocalizedMessage()); + } + return fileInfo; + } + + private ResponseValues uploadCssFile(VitroRequest vreq) throws FileUploadController.FileUploadException { + FileItem file = getUploadedFile(vreq); + + String mediaType = getMediaType(file); + if (!"text/css".equals(mediaType) && !"text/plain".equals(mediaType)) { + throw new FileUploadController.FileUploadException("Uploaded file is not a CSS or plain text file."); + } + + WebappDaoFactory webAppDaoFactory = vreq.getUnfilteredWebappDaoFactory(); + UploadedFileHelper fileHelper = new UploadedFileHelper(fileStorage, webAppDaoFactory, getServletContext()); + FileInfo fileInfo = createFile(file, "custom-style.css", fileHelper); + + ApplicationDao applicationDao = webAppDaoFactory.getApplicationDao(); + ApplicationBean applicationBean = applicationDao.getApplicationBean(); + applicationBean.setCustomCssPath(UrlBuilder.getUrl(fileInfo.getBytestreamAliasUrl())); + applicationDao.updateApplicationBean(applicationBean); + + return showMainStyleEditPage(vreq); + } + + private TemplateResponseValues removeCssFile(VitroRequest vreq) { + WebappDaoFactory webAppDaoFactory = vreq.getUnfilteredWebappDaoFactory(); + ApplicationDao applicationDao = webAppDaoFactory.getApplicationDao(); + ApplicationBean applicationBean = applicationDao.getApplicationBean(); + applicationBean.setCustomCssPath(null); + applicationDao.updateApplicationBean(applicationBean); + + return showMainStyleEditPage(vreq); + } + + private TemplateResponseValues showMainStyleEditPage(VitroRequest vreq) { + TemplateResponseValues rv = new TemplateResponseValues(TEMPLATE); + + rv.put(BODY_BACK_LOCATION, referrerHelper.getExitUrl(vreq)); + rv.put(BODY_FORM_ACTION_UPLOAD, UrlBuilder.getPath(URL_HERE, new ParamMap(PARAMETER_ACTION, ACTION_UPLOAD))); + rv.put(BODY_FORM_ACTION_REMOVE, UrlBuilder.getPath(URL_HERE, new ParamMap(PARAMETER_ACTION, ACTION_REMOVE))); + + return rv; + } + +} diff --git a/api/src/main/java/edu/cornell/mannlib/vitro/webapp/dao/VitroVocabulary.java b/api/src/main/java/edu/cornell/mannlib/vitro/webapp/dao/VitroVocabulary.java index f5e38bbc60..8e1d03716a 100644 --- a/api/src/main/java/edu/cornell/mannlib/vitro/webapp/dao/VitroVocabulary.java +++ b/api/src/main/java/edu/cornell/mannlib/vitro/webapp/dao/VitroVocabulary.java @@ -120,6 +120,7 @@ public class VitroVocabulary { public static final String PORTAL_ACKNOWLEGETEXT = vitroURI+"acknowledgeText"; public static final String PORTAL_COPYRIGHTURL = vitroURI+"copyrightURL"; public static final String PORTAL_COPYRIGHTANCHOR = vitroURI+"copyrightAnchor"; + public static final String PORTAL_CUSTOMCSSPATH = vitroURI+"customCssPath"; // reusing displayRank property above public static final String PORTAL_URLPREFIX = vitroURI + "urlPrefix"; diff --git a/api/src/main/java/edu/cornell/mannlib/vitro/webapp/dao/jena/ApplicationDaoJena.java b/api/src/main/java/edu/cornell/mannlib/vitro/webapp/dao/jena/ApplicationDaoJena.java index 9e1898020b..b44907a5e1 100644 --- a/api/src/main/java/edu/cornell/mannlib/vitro/webapp/dao/jena/ApplicationDaoJena.java +++ b/api/src/main/java/edu/cornell/mannlib/vitro/webapp/dao/jena/ApplicationDaoJena.java @@ -64,6 +64,8 @@ public ApplicationBean getApplicationBean() { appInd, APPLICATION_COPYRIGHTURL)); application.setThemeDir(getPropertyStringValue( appInd, APPLICATION_THEMEDIR)); + application.setCustomCssPath(getPropertyStringValue( + appInd, APPLICATION_CUSTOMCSSPATH)); } catch (Exception e) { log.error(e, e); } finally { @@ -104,6 +106,9 @@ public void updateApplicationBean(ApplicationBean application) { updatePropertyStringValue( appInd, APPLICATION_THEMEDIR, application.getThemeDir(), ontModel); + updatePropertyStringValue( + appInd, APPLICATION_CUSTOMCSSPATH, + application.getCustomCssPath(), ontModel); } catch (Exception e) { log.error(e, e); } finally { diff --git a/api/src/main/java/edu/cornell/mannlib/vitro/webapp/dao/jena/JenaBaseDaoCon.java b/api/src/main/java/edu/cornell/mannlib/vitro/webapp/dao/jena/JenaBaseDaoCon.java index 7830a01140..b728133aca 100644 --- a/api/src/main/java/edu/cornell/mannlib/vitro/webapp/dao/jena/JenaBaseDaoCon.java +++ b/api/src/main/java/edu/cornell/mannlib/vitro/webapp/dao/jena/JenaBaseDaoCon.java @@ -96,6 +96,7 @@ public JenaBaseDaoCon() { protected DatatypeProperty APPLICATION_ACKNOWLEGETEXT = _constModel.createDatatypeProperty(VitroVocabulary.PORTAL_ACKNOWLEGETEXT); protected DatatypeProperty APPLICATION_COPYRIGHTURL = _constModel.createDatatypeProperty(VitroVocabulary.PORTAL_COPYRIGHTURL); protected DatatypeProperty APPLICATION_COPYRIGHTANCHOR = _constModel.createDatatypeProperty(VitroVocabulary.PORTAL_COPYRIGHTANCHOR); + protected DatatypeProperty APPLICATION_CUSTOMCSSPATH = _constModel.createDatatypeProperty(VitroVocabulary.PORTAL_CUSTOMCSSPATH); protected AnnotationProperty ONTOLOGY_PREFIX_ANNOT = _constModel.createAnnotationProperty(VitroVocabulary.ONTOLOGY_PREFIX_ANNOT); diff --git a/api/src/main/java/edu/cornell/mannlib/vitro/webapp/filestorage/serving/FileServingServlet.java b/api/src/main/java/edu/cornell/mannlib/vitro/webapp/filestorage/serving/FileServingServlet.java index 9a647f345a..e2435f2ff4 100644 --- a/api/src/main/java/edu/cornell/mannlib/vitro/webapp/filestorage/serving/FileServingServlet.java +++ b/api/src/main/java/edu/cornell/mannlib/vitro/webapp/filestorage/serving/FileServingServlet.java @@ -9,6 +9,7 @@ import java.io.InputStream; import java.io.UnsupportedEncodingException; import java.net.URLDecoder; +import java.util.Objects; import javax.servlet.ServletException; import javax.servlet.ServletOutputStream; @@ -103,6 +104,10 @@ protected void doGet(HttpServletRequest rawRequest, */ response.setStatus(SC_OK); + if (Objects.equals(mimeType, "text/plain") && path.endsWith(".css")) { + mimeType = "text/css"; + } + if (mimeType != null) { response.setContentType(mimeType); } diff --git a/checkstyle-suppressions.xml b/checkstyle-suppressions.xml index 5e4693a52b..8b30cf2df4 100644 --- a/checkstyle-suppressions.xml +++ b/checkstyle-suppressions.xml @@ -345,6 +345,8 @@ + + diff --git a/webapp/src/main/webapp/css/vitro.css b/webapp/src/main/webapp/css/vitro.css index 3564107c26..ffe5b9384d 100644 --- a/webapp/src/main/webapp/css/vitro.css +++ b/webapp/src/main/webapp/css/vitro.css @@ -513,3 +513,9 @@ table#table-listing td { table#table-listing td a { word-wrap: break-word; } + +a.button.blue { + border: 1px solid #337ab7; + padding: 4px; + display: inline-block; +} diff --git a/webapp/src/main/webapp/templates/edit/specific/applicationBean_retry.jsp b/webapp/src/main/webapp/templates/edit/specific/applicationBean_retry.jsp index bcf003adf2..d634c9b337 100644 --- a/webapp/src/main/webapp/templates/edit/specific/applicationBean_retry.jsp +++ b/webapp/src/main/webapp/templates/edit/specific/applicationBean_retry.jsp @@ -40,6 +40,10 @@ +

+ Change CSS Styles +

+ " ${longField} maxlength="120" size="40" /> diff --git a/webapp/src/main/webapp/templates/freemarker/body/siteAdmin/siteAdmin-siteStyle.ftl b/webapp/src/main/webapp/templates/freemarker/body/siteAdmin/siteAdmin-siteStyle.ftl new file mode 100644 index 0000000000..a571b70c9e --- /dev/null +++ b/webapp/src/main/webapp/templates/freemarker/body/siteAdmin/siteAdmin-siteStyle.ftl @@ -0,0 +1,130 @@ +
+

Site Styles

+ +
+ +
+ +

Custom CSS Style

+ + <#if customCssPath?? && customCssPath != "null"> + + Download Custom CSS + +
+ + +

Note: Avoid using relative links inside the CSS file to ensure proper loading of resources.

+
+ Back + +
+
+ + +
\ No newline at end of file diff --git a/webapp/src/main/webapp/templates/freemarker/page/partials/head.ftl b/webapp/src/main/webapp/templates/freemarker/page/partials/head.ftl index 24c4cb9be7..c26ee7f976 100644 --- a/webapp/src/main/webapp/templates/freemarker/page/partials/head.ftl +++ b/webapp/src/main/webapp/templates/freemarker/page/partials/head.ftl @@ -24,4 +24,8 @@ an individual profile page. --> ${headContent!} +<#if customCssPath?? && customCssPath != "null"> + + +