From 1be5595aa4254c901732c7dbf9ba385885990bbc Mon Sep 17 00:00:00 2001 From: Justin Cleveland <clevelandj@wustl.edu> Date: Mon, 28 Mar 2016 18:27:49 -0500 Subject: [PATCH] Refactored Theme Management REST API and configuration UI --- .../org/nrg/xapi/rest/theme/ThemeApi.java | 170 +++++++++ .../nrg/xnat/configuration/ThemeConfig.java | 3 - .../xnat/restlet/extensions/ThemeRestlet.java | 344 ------------------ .../org/nrg/xnat/services/ThemeService.java | 141 +++++++ .../xnat/services/impl/ThemeServiceImpl.java | 323 ++++++++++++++++ .../xnat/turbine/modules/screens/Index.java | 24 +- .../xnat/turbine/modules/screens/Login.java | 44 +++ .../screens/XDATScreen_admin_options.java | 40 ++ .../modules/screens/XDATScreen_themes.java | 12 +- .../webapp/WEB-INF/conf/xnat-security.xml | 1 + src/main/webapp/scripts/themeManagement.js | 123 +++++++ .../navigations/HeaderIncludes.vm | 17 + .../screens/XDATScreen_themes.vm | 84 +---- 13 files changed, 894 insertions(+), 432 deletions(-) create mode 100644 src/main/java/org/nrg/xapi/rest/theme/ThemeApi.java delete mode 100644 src/main/java/org/nrg/xnat/restlet/extensions/ThemeRestlet.java create mode 100644 src/main/java/org/nrg/xnat/services/ThemeService.java create mode 100644 src/main/java/org/nrg/xnat/services/impl/ThemeServiceImpl.java create mode 100644 src/main/java/org/nrg/xnat/turbine/modules/screens/Login.java create mode 100644 src/main/java/org/nrg/xnat/turbine/modules/screens/XDATScreen_admin_options.java create mode 100644 src/main/webapp/scripts/themeManagement.js diff --git a/src/main/java/org/nrg/xapi/rest/theme/ThemeApi.java b/src/main/java/org/nrg/xapi/rest/theme/ThemeApi.java new file mode 100644 index 00000000..480d3f47 --- /dev/null +++ b/src/main/java/org/nrg/xapi/rest/theme/ThemeApi.java @@ -0,0 +1,170 @@ +/* + * org.nrg.xnat.turbine.modules.screens.ManageProtocol + * XNAT http://www.xnat.org + * Copyright (c) 2013, Washington University School of Medicine + * All Rights Reserved + * + * Released under the Simplified BSD. + * + * Author: Justin Cleveland <clevelandj@wustl.edu> + * Last modified 3/7/2016 11:52 AM + */ + +package org.nrg.xapi.rest.theme; + +import io.swagger.annotations.*; +import org.apache.commons.io.FileUtils; +import org.nrg.xapi.rest.NotFoundException; +import org.nrg.xdat.security.XDATUser; +import org.nrg.xft.security.UserI; +import org.nrg.xnat.configuration.ThemeConfig; +import org.nrg.xnat.services.ThemeService; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.web.bind.annotation.*; +import org.springframework.web.multipart.MultipartFile; + +import java.io.File; +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; + +@Api(description = "XNAT Theme Management API") +@RestController +@RequestMapping(value = "/theme") +public class ThemeApi { + private static final Logger _log = LoggerFactory.getLogger(ThemeApi.class); + + @Autowired + private ThemeService themeService; + + @ApiOperation(value = "Get the currently selected global theme or a role based theme if specified.", notes = "Use this to get the theme selected by the system administrator on the Theme Management page.", response = ThemeConfig.class, responseContainer = "ThemeConfig") + @ApiResponses({@ApiResponse(code = 200, message = "Reports the currently selected global theme (if there is one) and whether or not it's enabled."), @ApiResponse(code = 500, message = "Unexpected error")}) + @RequestMapping(value = {"/{role}"}, produces = {"application/json", "application/xml"}, method = RequestMethod.GET) + public ResponseEntity<ThemeConfig> themeGet(@ApiParam(value = "\"global\" or role name of currently set theme", required = true) @PathVariable("role") String role) { + if("global".equalsIgnoreCase(role)){ + return new ResponseEntity<>(themeService.getTheme(), HttpStatus.OK); + } + return new ResponseEntity<>(themeService.getTheme(role), HttpStatus.OK); + } + + @ApiOperation(value = "Get list of available themes.", notes = "Use this to get a list of all available themes on the XNAT system.", response = ThemeService.TypeOption.class, responseContainer = "List") + @ApiResponses({@ApiResponse(code = 200, message = "Reports the currently selected global theme (if there is one), whether or not it's enabled, and a list of available themes on the system in a JSON string."), @ApiResponse(code = 500, message = "Unexpected error")}) + @RequestMapping(produces = {"application/json", "application/xml"}, method = RequestMethod.GET) + public ResponseEntity<List<ThemeService.TypeOption>> themesGet() { + return new ResponseEntity<>(themeService.loadExistingThemes(), HttpStatus.OK); + } + + @ApiOperation(value = "Deletes the theme with the specified name.", notes = "Returns success on deletion. ", response = String.class) + @ApiResponses({@ApiResponse(code = 200, message = "Theme was successfully deleted."), @ApiResponse(code = 401, message = "Must be authenticated to access the XNAT REST API."), @ApiResponse(code = 403, message = "Not authorized to delete a theme."), @ApiResponse(code = 404, message = "Theme not found."), @ApiResponse(code = 500, message = "Unexpected error")}) + @RequestMapping(value = {"/{theme}"}, produces = {"application/json", "application/xml", "text/html"}, method = {RequestMethod.DELETE}) + public ResponseEntity<ThemeConfig> themeDelete(@ApiParam(value = "Name of the theme to delete", required = true) @PathVariable("theme") String theme) { + ThemeConfig themeConfig = null; + HttpStatus status = isPermitted(); + if (status != null) { + return new ResponseEntity<>(status); + } + if("null".equals(theme)){ + return new ResponseEntity<>(HttpStatus.NOT_FOUND); + } + if(!themeService.themeExists(theme)) { + return new ResponseEntity<>(HttpStatus.NOT_FOUND); + } + try { + File f = new File(themeService.getThemesPath() + File.separator + theme); + FileUtils.deleteDirectory(f); + if(!f.exists()) { + themeConfig = themeService.getTheme(); + String themeName = (themeConfig != null) ? themeConfig.getName() : null; + if (theme.equals(themeName)) { + themeService.setTheme((ThemeConfig) null); + } + } + } catch (Exception e) { + e.printStackTrace(); + return new ResponseEntity<>(HttpStatus.INTERNAL_SERVER_ERROR); + } + return themeConfig != null ? new ResponseEntity<>(themeConfig, HttpStatus.OK) : new ResponseEntity<ThemeConfig>(HttpStatus.NOT_FOUND); + } + + @ApiOperation(value = "Sets the current global theme to the one specified.", notes = "Returns the updated serialized theme object.", response = ThemeConfig.class) + @ApiResponses({@ApiResponse(code = 200, message = "Successfully updated the current global theme."), @ApiResponse(code = 401, message = "Must be authenticated to access the XNAT REST API."), @ApiResponse(code = 403, message = "Not authorized to create or update this user."), @ApiResponse(code = 404, message = "Theme not found."), @ApiResponse(code = 500, message = "Unexpected error")}) + @RequestMapping(value = {"/{theme}"}, produces = {"application/json", "application/xml", "text/html"}, method = {RequestMethod.PUT}) + public ResponseEntity<ThemeConfig> themePut(@ApiParam(value = "The name of the theme to select.", required = true) @PathVariable("theme") String theme, @RequestParam(value = "enabled", required=false, defaultValue="true") String enabled) throws NotFoundException { + HttpStatus status = isPermitted(); + if (status != null) { + return new ResponseEntity<>(status); + } + ThemeConfig themeConfig; + try { + boolean themeEnabled = true; + if("false".equalsIgnoreCase(enabled)){ + themeEnabled = false; + } + if("null".equals(theme)){ + theme = null; + } + themeConfig = themeService.setTheme(theme, themeEnabled); + } catch (ThemeService.ThemeNotFoundException e) { + _log.error(e.getInvalidTheme()+" not found."); + return new ResponseEntity<>(HttpStatus.NOT_FOUND); + } catch (Exception e) { + _log.error("An error occurred setting the theme " + theme, e); + return new ResponseEntity<>(HttpStatus.INTERNAL_SERVER_ERROR); + } + return new ResponseEntity<>(themeConfig, HttpStatus.OK); // TODO: fix the return on this. It's showing up as [object Object] on the page! + } + + @ApiOperation(value = "Accepts a multipart form with a zip file upload and extracts its contents in the theme system folder. If successful, the first (root) directory name (or theme name) unzipped is returned in the response. This will overwrite any other directories already existing with the same name without warning.", notes = "The structure of the zipped package must have only directories at it's root.", response = String.class) + @ApiResponses({@ApiResponse(code = 200, message = "Theme package successfully uploaded and extracted."), @ApiResponse(code = 401, message = "Must be authenticated to access the XNAT REST API."), @ApiResponse(code = 403, message = "Not authorized to upload a theme package."), @ApiResponse(code = 500, message = "Unexpected error")}) + @RequestMapping(produces = {"application/json"}, method = {RequestMethod.POST}) + public ResponseEntity<List<ThemeService.TypeOption>> themePostUpload(@ApiParam(value = "Multipart file object being uploaded", required = true) @RequestParam(value="themePackage", required=false) MultipartFile themePackage) { + HttpStatus status = isPermitted(); + if (status != null) { + return new ResponseEntity<>(status); + } + List<ThemeService.TypeOption> themeOptions = new ArrayList<>(); + try { + if(!themePackage.getContentType().contains("zip")) { + String error = "No valid files were uploaded. Theme package must be of type: application/x-zip-compressed"; + _log.error(error); + return new ResponseEntity<>(HttpStatus.BAD_REQUEST); + } + List<String> dirs = themeService.extractTheme(themePackage.getInputStream()); + for (String dir : dirs) { + themeOptions.add(new ThemeService.TypeOption(dir, dir)); + } + } catch (IOException e) { + e.printStackTrace(); + String error = "An error occurred extracting the theme package"; + _log.error(error, e); + return new ResponseEntity<>(HttpStatus.INTERNAL_SERVER_ERROR); + } catch (Exception e) { + e.printStackTrace(); + String error = "An unknown error occurred accepting the theme package"; + _log.error(error, e); + return new ResponseEntity<>(HttpStatus.INTERNAL_SERVER_ERROR); + } + return new ResponseEntity<>(themeOptions, HttpStatus.OK); + } + + private UserI getSessionUser() { + Object principal = SecurityContextHolder.getContext().getAuthentication().getPrincipal(); + if ((principal instanceof UserI)) { + return (UserI) principal; + } + return null; + } + + private HttpStatus isPermitted() { + UserI sessionUser = getSessionUser(); + if ((sessionUser instanceof XDATUser)) { + return ((XDATUser) sessionUser).isSiteAdmin() ? null : HttpStatus.FORBIDDEN; + } + return null; + } +} diff --git a/src/main/java/org/nrg/xnat/configuration/ThemeConfig.java b/src/main/java/org/nrg/xnat/configuration/ThemeConfig.java index f0337e26..ca054e8c 100644 --- a/src/main/java/org/nrg/xnat/configuration/ThemeConfig.java +++ b/src/main/java/org/nrg/xnat/configuration/ThemeConfig.java @@ -28,9 +28,6 @@ public class ThemeConfig { */ public ThemeConfig() { } - public ThemeConfig(String themeName) { - this.name = themeName; - } public ThemeConfig(String themeName, String themePath, boolean enabled) { this.name = themeName; this.path = themePath; diff --git a/src/main/java/org/nrg/xnat/restlet/extensions/ThemeRestlet.java b/src/main/java/org/nrg/xnat/restlet/extensions/ThemeRestlet.java deleted file mode 100644 index d072e0c0..00000000 --- a/src/main/java/org/nrg/xnat/restlet/extensions/ThemeRestlet.java +++ /dev/null @@ -1,344 +0,0 @@ -/* - * org.nrg.xnat.turbine.modules.screens.ManageProtocol - * XNAT http://www.xnat.org - * Copyright (c) 2013, Washington University School of Medicine - * All Rights Reserved - * - * Released under the Simplified BSD. - * - * Author: Justin Cleveland <clevelandj@wustl.edu> - * Last modified 1/25/2016 1:52 PM - */ - -package org.nrg.xnat.restlet.extensions; - -import com.fasterxml.jackson.core.JsonProcessingException; -import com.fasterxml.jackson.databind.ObjectMapper; -import org.apache.commons.fileupload.*; -import org.apache.commons.io.FileUtils; -import org.apache.commons.logging.Log; -import org.apache.commons.logging.LogFactory; -import org.nrg.action.ClientException; -import org.nrg.xdat.security.helpers.Roles; -import org.nrg.xnat.configuration.ThemeConfig; -import org.nrg.xnat.restlet.XnatRestlet; -import org.nrg.xnat.restlet.resources.SecureResource; -import org.nrg.xnat.restlet.util.FileWriterWrapperI; -import org.restlet.Context; -import org.restlet.data.MediaType; -import org.restlet.data.Request; -import org.restlet.data.Response; -import org.restlet.data.Status; -import org.restlet.resource.Representation; -import org.restlet.resource.StringRepresentation; -import org.restlet.resource.Variant; - -import java.io.*; -import java.util.ArrayList; -import java.util.List; -import java.util.zip.ZipEntry; -import java.util.zip.ZipInputStream; - -/** - * Created by jcleve01 on 1/26/2016. - */ -@XnatRestlet({"/theme"}) -public class ThemeRestlet extends SecureResource { - private static final Log _log = LogFactory.getLog(ThemeRestlet.class); - private static String themesPath; - protected final ObjectMapper mapper = new ObjectMapper(); - private static final int FILE_BUFFER_SIZE = 4096; - private File themeFile = null; - - public ThemeRestlet(Context context, Request request, Response response) throws UnsupportedEncodingException { - super(context, request, response); - setModifiable(true); - getVariants().add(new Variant(MediaType.APPLICATION_JSON)); - themesPath = this.getHttpSession().getServletContext().getRealPath(File.separator)+"themes"; - themeFile = new File(themesPath + File.separator + "theme.json"); - File checkThemesPath = new File(themesPath); - if (!checkThemesPath.exists()) { - checkThemesPath.mkdir(); - } - } - - @Override - public boolean allowDelete() { - return true; - } - - /** // TODO: convert to swagger doc tags - * Administrator use only - * Deletes the entire theme package folder specified by the "theme" query parameter. If it happens to be the - * currently applied theme, the current theme is set to null and the default XNAT theme will be restored. - */ - @Override - public void handleDelete() { - String theme = this.getQueryVariable("theme"); - if(theme != null && !theme.isEmpty()) { - File f = new File(themesPath + File.separator + theme); - if (Roles.isSiteAdmin(user)) { - try { - FileUtils.deleteDirectory(f); - ThemeConfig tc = getTheme(); - String themeName = (tc!=null)?tc.getName():null; - if(theme != null && theme.equals(themeName)){ - setTheme(null); - } - } catch (Exception e) { - e.printStackTrace(); - getResponse().setStatus(Status.SERVER_ERROR_INTERNAL, "Theme directory deletion failed."); - } - } else { - getResponse().setStatus(Status.CLIENT_ERROR_UNAUTHORIZED, "Only site administrators can delete themes."); - } - } - } - - @Override - public boolean allowPut() { - return true; - } - - /** // TODO: convert to swagger doc tags - * Administrator use only - * Sets the currently selected global theme configuration package by updating the theme.json file under the - * application's theme folder and caches a copy of it in the app server's application context for reference by any - * other pages or servlets that may need to alter their styles and behaviors to fit the overriding theme. - */ - @Override - public void handlePut() { - if (Roles.isSiteAdmin(user)) { - boolean enabled = true; - String name = this.getQueryVariable("theme"), path = themesPath + File.separator + name; - if("true".equals(this.getQueryVariable("disabled"))){ - enabled = false; - } - setTheme(new ThemeConfig(name, themesPath, enabled)); - } else { - getResponse().setStatus(Status.CLIENT_ERROR_UNAUTHORIZED, "Only site administrators can set the site theme."); - } - } - - @Override - public boolean allowPost() { - return true; - } - - /** // TODO: convert to swagger doc tags - * Administrator use only - * Accepts a multipart form with a zip file upload and extracts its contents in the theme system folder. - * The structure of the zipped package must have only directories at it's root - * If successful, the first (root) directory name (or theme name) unzipped is returned in the response. - * This will overwrite any other directories already existing with the same name without warning. - */ - @Override - public void handlePost() { //Representation entity) { // Upload Theme package - Representation result = null; - if (Roles.isSiteAdmin(user)) { - try { - String firstDirName = ""; - FileWriterWrapperI fw; - List<FileWriterWrapperI> fws = this.getFileWriters(); - if (fws.size() == 0) { - this.getResponse().setStatus(Status.CLIENT_ERROR_BAD_REQUEST); - getResponse().setEntity("No valid files were uploaded.", MediaType.TEXT_PLAIN); - return; - } - if (fws.size() > 1) { - this.getResponse().setStatus(Status.CLIENT_ERROR_BAD_REQUEST); - getResponse().setEntity("Theme importer is limited to one uploaded theme package per request.", MediaType.TEXT_PLAIN); - return; - } - fw = fws.get(0); //TODO: inspect this fw object a little more for metadata - // TODO: validate zipped media type (fail otherwise) - final InputStream is = fw.getInputStream(); - ZipInputStream zipIn = new ZipInputStream(is); - ZipEntry entry = zipIn.getNextEntry(); - int dirCount = 0; - while (entry != null) { // iterate over entries in the zip file - String filePath = themesPath + File.separator + entry.getName(); - if (!entry.isDirectory()) { // if the entry is a file, extract it // TODO: Make sure we get a directory the first iteration through (fail otherwise) so that no files get dumped in the root themes directory - extractFile(zipIn, filePath); - } else { // if the entry is a directory, make the directory - if(dirCount == 0) { - firstDirName = entry.getName(); - int slashIndex = firstDirName.indexOf('/'); - if(slashIndex>1){ - firstDirName = firstDirName.substring(0, slashIndex); - } - } - dirCount++; - File dir = new File(filePath); - dir.mkdir(); - } - zipIn.closeEntry(); - entry = zipIn.getNextEntry(); - } - zipIn.close(); - is.close(); - getResponse().setEntity(firstDirName, MediaType.TEXT_PLAIN); - } catch (FileUploadBase.InvalidContentTypeException icte){ - this.getResponse().setStatus(Status.CLIENT_ERROR_BAD_REQUEST); - getResponse().setEntity("Invalid Content Type: "+icte.getMessage(), MediaType.TEXT_PLAIN); - } catch (FileUploadException fue) { - this.getResponse().setStatus(Status.CLIENT_ERROR_BAD_REQUEST); - getResponse().setEntity("File Upload Exception: " + fue.getMessage(), MediaType.TEXT_PLAIN); - } catch (ClientException e) { - e.printStackTrace(); - } catch (IOException e) { - e.printStackTrace(); - } - } else { - getResponse().setStatus(Status.CLIENT_ERROR_UNAUTHORIZED, "Only site administrators can upload new themes."); - } - } - - /** - * Extracts a zip entry (file entry) - * @param zip - * @param path - * @throws IOException - */ - private void extractFile(ZipInputStream zip, String path) throws IOException { - BufferedOutputStream os = new BufferedOutputStream(new FileOutputStream(path)); - byte[] bytes = new byte[FILE_BUFFER_SIZE]; - int length = 0; - while ((length = zip.read(bytes)) != -1) { - os.write(bytes, 0, length); - } - os.close(); - } - - /** // TODO: convert to swagger doc tags - * Reports the currently selected global theme (if there is one), whether or not it's enabled, and a list of - * available themes on the system in a JSON string. - */ - @Override - public Representation represent(Variant variant){ - String themeOptions = null, themeName = null; - boolean themeEnabled = false; - ThemeConfig theme = getTheme(); - try { - themeOptions = mapper.writeValueAsString(loadExistingThemes().toArray()); - } catch (JsonProcessingException e) { - e.printStackTrace(); - } - if(theme != null) { - themeName="\""+theme.getName()+"\""; - themeEnabled=theme.isEnabled(); - } - return new StringRepresentation( - "{\"theme\":"+themeName+", \"enabled\":"+themeEnabled+", \"themeOptions\":"+themeOptions+"}", - MediaType.APPLICATION_JSON); - } - - /** - * Loads the system theme options - * @return The list of the available theme packages (folder names) available under the system themes directory - */ - public ArrayList<TypeOption> loadExistingThemes() { - ArrayList<TypeOption> themeOptions = new ArrayList<>(); - themeOptions.add(new TypeOption(null, "None")); - File f = new File(themesPath); // current directory - FileFilter directoryFilter = new FileFilter() { - public boolean accept(File file) { - return file.isDirectory(); - } - }; - File[] files = f.listFiles(directoryFilter); - if(files != null) { - for (File file : files) { - if (file.isDirectory()) { - themeOptions.add(new TypeOption(file.getName(), file.getName())); - } - } - } - return themeOptions; - } - - /** - * Gets the currently selected system theme from an application servlet context cache, or secondarily from the - * theme.json file in the themes folder. - * @return The currently selected system theme configuration - */ - public ThemeConfig getTheme() { - ThemeConfig themeConfig = null, cachedTheme = (ThemeConfig) this.getHttpSession().getServletContext().getAttribute("theme"); - if(cachedTheme != null){ - return cachedTheme; - } else { // Read the last saved theme selection from the theme.json file in the themes - if (themeFile.exists()) { // directory in the event it can't be found in the application context. - try { // (ie. the server was just started/restarted) - BufferedReader reader = new BufferedReader(new FileReader(themeFile)); - StringBuilder sb = new StringBuilder(); - String line; - while ((line = reader.readLine()) != null) { - sb.append(line).append("\n"); - } - reader.close(); - String contents = sb.toString(); - themeConfig = mapper.readValue(contents, ThemeConfig.class); - } catch (IOException e) { - e.printStackTrace(); - } - } - setTheme(themeConfig); - } - return themeConfig; - } - - /** - * Sets the currently selected system theme in the theme.json file in the web application's themes folder and - * stores it in the application servlet context cache - * @param themeConfig the theme configuration object to apply - */ - public void setTheme(ThemeConfig themeConfig) { - try { - if (themeConfig != null) { - String themeJson = mapper.writeValueAsString(themeConfig); - if (!themeFile.exists()) { - themeFile.createNewFile(); - } - FileWriter writer = new FileWriter(themeFile); - writer.write(themeJson); - writer.flush(); - writer.close(); - } else { - themeFile.delete(); - if (themeFile.exists()) { // Backup hack in case the file deletion does not - FileWriter writer = new FileWriter(themeFile); // succeed due to a file lock which sometimes - writer.write("{\"name\":null}"); // strangely happens in Windows - writer.flush(); // ...in that case wipe it out. - writer.close(); - } - } - } catch (JsonProcessingException e) { - e.printStackTrace(); - // TODO: rethrow this and respond as an internal server error - } catch (IOException e) { - e.printStackTrace(); - } - this.getHttpSession().getServletContext().setAttribute("theme", themeConfig); - } - - /** - * Helper class to organize the available themes for display in a select dropdown form - */ - public static class TypeOption implements Comparable<TypeOption> { - String value, label; - private TypeOption(String value, String label) { - this.value = value; - this.label = label; - } - public String getValue() { - return value; - } - public String getLabel() { - return label; - } - @Override - public int compareTo(TypeOption that) { - return this.label.compareToIgnoreCase(that.label); - } - } -} diff --git a/src/main/java/org/nrg/xnat/services/ThemeService.java b/src/main/java/org/nrg/xnat/services/ThemeService.java new file mode 100644 index 00000000..47b61271 --- /dev/null +++ b/src/main/java/org/nrg/xnat/services/ThemeService.java @@ -0,0 +1,141 @@ +/* + * org.nrg.xnat.turbine.modules.screens.ManageProtocol + * XNAT http://www.xnat.org + * Copyright (c) 2013, Washington University School of Medicine + * All Rights Reserved + * + * Released under the Simplified BSD. + * + * Author: Justin Cleveland <clevelandj@wustl.edu> (jcleve01) + * Last modified 2/29/2016 11:20 AM + */ +package org.nrg.xnat.services; + +import org.jetbrains.annotations.NotNull; +import org.nrg.xnat.configuration.ThemeConfig; +import org.springframework.stereotype.Service; + +import java.io.*; +import java.util.List; + +@Service +public interface ThemeService { + +// abstract public void postServiceConstruction(); + + /** + * Returns the system theme file directory for reference by other XNAT modules. + * @return the system theme file directory + */ + String getThemesPath(); + + /** + * Gets the currently selected global system theme from a cache for a specific user role, or secondarily from the theme.json file in the themes folder. + * @param role the name of the user role to fetch from the current global theme + * @return The currently selected system theme configuration + */ + ThemeConfig getTheme(String role); + + /** + * Gets the currently selected global system theme from a cache, or secondarily from the theme.json file in the themes folder. + * @return The currently selected global system theme configuration + */ + ThemeConfig getTheme(); + + /** + * Searches the theme directory if a global theme is applied and returns a path string to the referenced page to redirect to. + * If no global theme is selected or no overriding page is found the calling method should continue with it's default XNAT behavior. + * @return a path string the referenced page if found. Otherwise returns null. + */ + String getThemePage(String pageName); + + /** + * Searches the theme directory if a global theme is applied and returns a path string to the referenced theme and matching type to redirect to. + * If no global theme is selected or no overriding page with specified type is found the calling method should continue with it's default XNAT behavior. + * @return a path string the referenced theme and type if found. Otherwise returns null. + */ + String getThemePage(String pageName, String type); + + /** + * Sets the currently selected system theme in the theme.json file in the web application's themes folder and caches it. + * @param themeConfig the theme configuration object to apply + */ + ThemeConfig setTheme(ThemeConfig themeConfig) throws ThemeNotFoundException; + + /** + * Sets the currently selected system theme in the theme.json file in the web application's themes folder and caches it. + * @param name the theme name. Creates a theme configuration object with it applying defaults + */ + ThemeConfig setTheme(String name) throws ThemeNotFoundException; + + /** + * Sets the currently selected system theme in the theme.json file in the web application's themes folder and caches it. + * Creates a theme configuration object with it applying a defaults path + * @param name the theme name. + * @param enabled flag specifying whether or not the theme should be active. + */ + ThemeConfig setTheme(String name, boolean enabled) throws ThemeNotFoundException; + + /** + * Sets the currently selected system theme in the theme.json file in the web application's themes folder and caches it. + * @param name the theme name. + * @param path base theme directory path. + * @param enabled flag specifying whether or not the theme should be active. + */ + ThemeConfig setTheme(String name, String path, boolean enabled) throws ThemeNotFoundException; + + /** + * Loads the system theme options + * @return The list of the available theme packages (folder names) available under the system themes directory + */ + List<TypeOption> loadExistingThemes(); + + /** + * Checks if the specified theme exists. + * @param name the name of the theme to look for + * @return true if it could be found in the system theme directory + */ + boolean themeExists(String name); + + /** + * Extracts a zipped theme package from an given InputStream. + * @param inputStream from which to read the zipped data + * @return List of root level directories (theme names) that were extracted + * @throws IOException + */ + List<String> extractTheme(InputStream inputStream) throws IOException; + + /** + * Helper exception to report more specific errors + */ + class ThemeNotFoundException extends Exception{ + private String invalidTheme; + public ThemeNotFoundException(String invalidTheme) { + this.invalidTheme = invalidTheme; + } + public String getInvalidTheme() { + return invalidTheme; + } + } + + /** + * Helper class to organize the available themes for display in a select dropdown form + */ + class TypeOption implements Comparable<TypeOption> { + String value, label; + public TypeOption(String value, String label) { + this.value = value; + this.label = label; + } + public String getValue() { + return value; + } + public String getLabel() { + return label; + } + @Override + public int compareTo(@NotNull TypeOption that) { + return this.label.compareToIgnoreCase(that.label); + } + } +} \ No newline at end of file diff --git a/src/main/java/org/nrg/xnat/services/impl/ThemeServiceImpl.java b/src/main/java/org/nrg/xnat/services/impl/ThemeServiceImpl.java new file mode 100644 index 00000000..f72a2cfa --- /dev/null +++ b/src/main/java/org/nrg/xnat/services/impl/ThemeServiceImpl.java @@ -0,0 +1,323 @@ +/* + * org.nrg.xnat.turbine.modules.screens.ManageProtocol + * XNAT http://www.xnat.org + * Copyright (c) 2013, Washington University School of Medicine + * All Rights Reserved + * + * Released under the Simplified BSD. + * + * Author: Justin Cleveland <clevelandj@wustl.edu> (jcleve01) + * Last modified 2/30/2016 11:20 AM + */ +package org.nrg.xnat.services.impl; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import org.apache.commons.lang.ArrayUtils; +import org.apache.commons.lang.StringUtils; +import org.nrg.xnat.configuration.ThemeConfig; +import org.nrg.xnat.services.ThemeService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import javax.annotation.PostConstruct; +import javax.servlet.ServletContext; +import java.io.*; +import java.util.ArrayList; +import java.util.List; +import java.util.zip.ZipEntry; +import java.util.zip.ZipInputStream; + +@Service +public class ThemeServiceImpl implements ThemeService { + private static String webRelativeThemePath="themes"; + private static String themesPath; + private static ThemeConfig themeConfig = null; + private static File themeFile = null; + protected final ObjectMapper mapper = new ObjectMapper(); + private static final int FILE_BUFFER_SIZE = 4096; + + @Autowired + private ServletContext servletContext; + + @PostConstruct + public void postServiceConstruction(){ + themesPath = servletContext.getRealPath(File.separator)+webRelativeThemePath; + themeFile = new File(themesPath + File.separator + "theme.json"); + File checkThemesPath = new File(themesPath); + if (!checkThemesPath.exists()) { + checkThemesPath.mkdir(); + } +System.out.println("Theme Path: "+themeFile); + servletContext.setAttribute("ThemeService", this); // This is probably a terrible way to attempt to do this. We would ideally add an instance of ThemeService to the XDAT class + } + + public String getThemesPath() { + return themesPath; + } + + /** + * Gets the currently selected system theme from an application servlet context cache, or secondarily from the + * theme.json file in the themes folder. + * @return The currently selected system theme configuration + */ + public ThemeConfig getTheme(String role) { + if(themeConfig != null){ + return themeConfig; + } else { // Read the last saved theme selection from the theme.json file in the themes + if (themeFile.exists()) { // directory in the event it can't be found in the application context. + try { // (ie. the server was just started/restarted) + BufferedReader reader = new BufferedReader(new FileReader(themeFile)); + StringBuilder sb = new StringBuilder(); + String line; + while ((line = reader.readLine()) != null) { + sb.append(line).append("\n"); + } + reader.close(); + String contents = sb.toString(); + themeConfig = mapper.readValue(contents, ThemeConfig.class); + } catch (IOException e) { + e.printStackTrace(); + } + } + try { + setTheme(themeConfig); + } catch (ThemeNotFoundException e) { + e.printStackTrace(); + } + } + if(role != null){ + // TODO: implement search through the roles array in the ThemeConfig object for a matching ThemeConfig object for the specified role + } + return themeConfig; + } + public ThemeConfig getTheme() { + return getTheme(null); + } + + /** + * Searches the theme directory if a global theme is applied and returns a path string to the referenced page to redirect to. + * If no global theme is selected or no overriding page is found the calling method should continue with it's default XNAT behavior. + * @return a path string the referenced page if found. Otherwise returns null. + */ + public String getThemePage(String pageName) { + return getThemePage(pageName, null); + } + + /** + * Searches the theme directory if a global theme is applied and returns a path string to the referenced theme and matching type to redirect to. + * If no global theme is selected or no overriding page with specified type is found the calling method should continue with it's default XNAT behavior. + * @return a path string the referenced theme and type if found. Otherwise returns null. + */ + public String getThemePage(String pageName, String type) { + String pagePath; + ThemeConfig theme = getTheme(); + if(theme == null){ + return null; + } else if (pageName == null){ + return null; + } else { // Read the last saved theme selection from the theme.json file in the themes + pagePath = checkThemeFileExists(theme, pageName, type); + } + return pagePath; + } + + /** + * Checks for the existence of a file name with a given set of accepted file extensions in the theme directory + * and returns a relative web path string the referenced page if found. + * @return a relative web path string prioritized by extension to the referenced page if found. Otherwise returns null. + */ + private String checkThemeFileExists(ThemeConfig theme, String pageName) { + return checkThemeFileExists(theme, pageName, null); + } + private String checkThemeFileExists(ThemeConfig theme, String pageName, String type) { + String pagePath = null, typeSep = type + "s" + File.separator; + String[] extensions = new String[]{}; + String[] pageExts = new String[]{"jsp", "vm", "htm", "html"}; + String[] scriptExts = new String[]{"js"}; + String[] styleExts = new String[]{"css"}; + if("page".equals(type)){ + extensions = (String[]) ArrayUtils.addAll(extensions, pageExts); + } + if("script".equals(type)){ + extensions = (String[]) ArrayUtils.addAll(extensions, scriptExts); + } + if("style".equals(type)){ + extensions = (String[]) ArrayUtils.addAll(extensions, styleExts); + } + if(type == null){ + typeSep = ""; + extensions = (String[]) ArrayUtils.addAll(extensions, pageExts); + extensions = (String[]) ArrayUtils.addAll(extensions, scriptExts); + extensions = (String[]) ArrayUtils.addAll(extensions, styleExts); + } + for (String ext : extensions) { + File themePageFile = new File(theme.getPath() + File.separator + typeSep + pageName + "." + ext); + if(themePageFile.exists()) { + if(type != null){ + typeSep = type + "s/"; // This is awful and should be set once up above + } + pagePath = "/" + webRelativeThemePath + "/" + theme.getName() + "/" + typeSep + pageName + "." + ext; + break; + } + } + return pagePath; + } + + /** + * Sets the currently selected system theme in the theme.json file in the web application's themes folder and caches it. + * @param themeConfig the theme configuration object to apply + */ + public ThemeConfig setTheme(ThemeConfig themeConfig) throws ThemeNotFoundException { + try { + if (themeConfig == null){ + themeConfig = new ThemeConfig(); + } + if(themeExists(themeConfig.getName())) { + String themeJson = mapper.writeValueAsString(themeConfig); + if (!themeFile.exists()) { + themeFile.createNewFile(); + } + FileWriter writer = new FileWriter(themeFile); + writer.write(themeJson); + writer.flush(); + writer.close(); + ThemeServiceImpl.themeConfig = themeConfig; + } else { + throw new ThemeNotFoundException(themeConfig.getName()); + } + } catch (JsonProcessingException e) { + e.printStackTrace(); + // TODO: rethrow this and respond as an internal server error + } catch (IOException e) { + e.printStackTrace(); + // TODO: rethrow this and respond as an internal server error + } + return themeConfig; + } + + /** + * Sets the currently selected system theme in the theme.json file in the web application's themes folder and caches it. + * @param name the theme name. Creates a theme configuration object with it applying defaults + */ + public ThemeConfig setTheme(String name) throws ThemeNotFoundException { + return setTheme(name, true); + } + + /** + * Sets the currently selected system theme in the theme.json file in the web application's themes folder and caches it. + * Creates a theme configuration object with it applying a defaults path + * @param name the theme name. + * @param enabled flag specifying whether or not the theme should be active. + */ + public ThemeConfig setTheme(String name, boolean enabled) throws ThemeNotFoundException { + return setTheme(new ThemeConfig(name, themesPath + File.separator + name, enabled)); + } + + /** + * Sets the currently selected system theme in the theme.json file in the web application's themes folder and caches it. + * @param name the theme name. + * @param path base theme directory path. + * @param enabled flag specifying whether or not the theme should be active. + */ + public ThemeConfig setTheme(String name, String path, boolean enabled) throws ThemeNotFoundException { + return setTheme(new ThemeConfig(name, path, enabled)); + } + + /** + * Loads the system theme options + * @return The list of the available theme packages (folder names) available under the system themes directory + */ + public List<TypeOption> loadExistingThemes() { + ArrayList<TypeOption> themeOptions = new ArrayList<>(); + themeOptions.add(new TypeOption(null, "None")); + File f = new File(themesPath); // current directory + FileFilter directoryFilter = new FileFilter() { + public boolean accept(File file) { + return file.isDirectory(); + } + }; + File[] files = f.listFiles(directoryFilter); + if(files != null) { + for (File file : files) { + if (file.isDirectory()) { + themeOptions.add(new TypeOption(file.getName(), file.getName())); + } + } + } + return themeOptions; + } + + /** + * Checks if the specified theme exists. + * @param name the name of the theme to look for + * @return true if it could be found in the system theme directory + */ + public boolean themeExists(String name) { + if(name == null) { + return true; + } else if(StringUtils.isEmpty(name)){ + return false; + } else { + List<TypeOption> themeList = loadExistingThemes(); + for (TypeOption to: themeList) { + if(name.equals(to.getValue())){ + return true; + } + } + } + return false; + } + + /** + * Extracts a zipped theme package from an given InputStream. + * @param inputStream from which to read the zipped data + * @return List of root level directories (theme names) that were extracted + * @throws IOException + */ + public List<String> extractTheme(InputStream inputStream) throws IOException { + ArrayList rootDirs = new ArrayList(); + ZipInputStream zipIn = new ZipInputStream(inputStream); + ZipEntry entry = zipIn.getNextEntry(); + while (entry != null) { // iterate over entries in the zip file + String filePath = this.getThemesPath() + File.separator + entry.getName(); + if (!entry.isDirectory()) { // if the entry is a file, extract it // TODO: Make sure we get a directory the first iteration through (fail otherwise) so that no files get dumped in the root themes directory + this.extractFile(zipIn, filePath); + } else { // if the entry is a directory, make the directory + String rootDir = ""; + rootDir = entry.getName(); + int slashIndex = rootDir.indexOf('/'); + if(slashIndex>1){ + int nextSlashIndex = rootDir.indexOf('/', slashIndex+1); + if(nextSlashIndex<0) { + rootDir = rootDir.substring(0, slashIndex); + rootDirs.add(rootDir); + } + } + File dir = new File(filePath); + dir.mkdir(); + } + zipIn.closeEntry(); + entry = zipIn.getNextEntry(); + } + zipIn.close(); + inputStream.close(); + return rootDirs; + } + + /** + * Extracts a single zip entry (file entry) + * @param zip zip input stream to extract it from + * @param path to the file within the zip package + * @throws IOException + */ + private void extractFile(ZipInputStream zip, String path) throws IOException { + BufferedOutputStream os = new BufferedOutputStream(new FileOutputStream(path)); + byte[] bytes = new byte[FILE_BUFFER_SIZE]; + int length; + while ((length = zip.read(bytes)) != -1) { + os.write(bytes, 0, length); + } + os.close(); + } +} \ No newline at end of file diff --git a/src/main/java/org/nrg/xnat/turbine/modules/screens/Index.java b/src/main/java/org/nrg/xnat/turbine/modules/screens/Index.java index b8d89bc6..64953b9e 100644 --- a/src/main/java/org/nrg/xnat/turbine/modules/screens/Index.java +++ b/src/main/java/org/nrg/xnat/turbine/modules/screens/Index.java @@ -14,19 +14,41 @@ import java.util.Date; import org.apache.turbine.util.RunData; import org.apache.velocity.context.Context; +import org.nrg.xdat.XDAT; import org.nrg.xdat.security.helpers.UserHelper; import org.nrg.xdat.turbine.modules.screens.SecureScreen; import org.nrg.xdat.turbine.utils.TurbineUtils; import org.nrg.xft.db.PoolDBUtils; import org.nrg.xft.security.UserI; import org.nrg.xnat.helpers.prearchive.PrearcDatabase; + +import org.nrg.xnat.services.ThemeService; import org.nrg.xnat.turbine.utils.ProjectAccessRequest; public class Index extends SecureScreen { @Override protected void doBuildTemplate(RunData data, Context context) throws Exception { - + ThemeService themeService = XDAT.getContextService().getBean(ThemeService.class); +// String themedLandingPath = themeService.getThemePage("Landing"); +// if(themedLandingPath != null) { +// doRedirect(data, themedLandingPath); +// data.setRedirectURI(themedLandingPath); +// } + String themedRedirect = themeService.getThemePage("Landing"); // put all this in a method in the theme service with an optional requested page parameter + if(themedRedirect != null) { + context.put("themedRedirect", themedRedirect); + return; + } + String themedStyle = themeService.getThemePage("theme", "style"); + if(themedStyle != null) { + context.put("themedStyle", themedStyle); + } + String themedScript = themeService.getThemePage("theme", "script"); + if(themedScript != null) { + context.put("themedScript", themedScript); + } + UserI user = TurbineUtils.getUser(data); if(((String)org.nrg.xdat.turbine.utils.TurbineUtils.GetPassedParameter("node",data))!=null){ diff --git a/src/main/java/org/nrg/xnat/turbine/modules/screens/Login.java b/src/main/java/org/nrg/xnat/turbine/modules/screens/Login.java new file mode 100644 index 00000000..8b2785b2 --- /dev/null +++ b/src/main/java/org/nrg/xnat/turbine/modules/screens/Login.java @@ -0,0 +1,44 @@ +/* + * org.nrg.xnat.turbine.modules.screens.ManageProtocol + * XNAT http://www.xnat.org + * Copyright (c) 2013, Washington University School of Medicine + * All Rights Reserved + * + * Released under the Simplified BSD. + * + * Author: Justin Cleveland <clevelandj@wustl.edu> (jcleve01) + * Last modified 1/22/2016 3:20 PM + */ +package org.nrg.xnat.turbine.modules.screens; + +import org.apache.log4j.Logger; +import org.apache.turbine.util.RunData; +import org.apache.velocity.context.Context; +import org.nrg.xdat.XDAT; +import org.nrg.xnat.services.ThemeService; + +public class Login extends org.nrg.xdat.turbine.modules.screens.Login { + public final static Logger logger = Logger.getLogger(XDATScreen_themes.class); + @Override + protected void doBuildTemplate(RunData data, Context context) throws Exception { + ThemeService themeService = XDAT.getContextService().getBean(ThemeService.class); +// String themedLoginPath = themeService.getThemePage("Login"); +// if(themedLoginPath != null) { +// doRedirect(data, themedLoginPath); +// data.setRedirectURI(themedLoginPath); +// } + String themedRedirect = themeService.getThemePage("Login"); + if(themedRedirect != null) { + context.put("themedRedirect", themedRedirect); + return; + } + String themedStyle = themeService.getThemePage("theme", "style"); + if(themedStyle != null) { + context.put("themedStyle", themedStyle); + } + String themedScript = themeService.getThemePage("theme", "script"); + if(themedScript != null) { + context.put("themedScript", themedScript); + } + } +} \ No newline at end of file diff --git a/src/main/java/org/nrg/xnat/turbine/modules/screens/XDATScreen_admin_options.java b/src/main/java/org/nrg/xnat/turbine/modules/screens/XDATScreen_admin_options.java new file mode 100644 index 00000000..3bb1cb82 --- /dev/null +++ b/src/main/java/org/nrg/xnat/turbine/modules/screens/XDATScreen_admin_options.java @@ -0,0 +1,40 @@ +/* + * org.nrg.xnat.turbine.modules.screens.ManageProtocol + * XNAT http://www.xnat.org + * Copyright (c) 2013, Washington University School of Medicine + * All Rights Reserved + * + * Released under the Simplified BSD. + * + * Author: Justin Cleveland <clevelandj@wustl.edu> + * Last modified 1/22/2016 3:20 PM + */ + +package org.nrg.xnat.turbine.modules.screens; + +import org.apache.log4j.Logger; +import org.apache.turbine.util.RunData; +import org.apache.velocity.context.Context; +import org.nrg.xdat.XDAT; +import org.nrg.xdat.turbine.modules.screens.SecureScreen; +import org.nrg.xnat.services.ThemeService; + + +/** + * Created by jcleve01 on 1/22/2016. + */ +public class XDATScreen_admin_options extends SecureScreen { + public final static Logger logger = Logger.getLogger(XDATScreen_admin_options.class); + @Override + protected void doBuildTemplate(RunData data, Context context) throws Exception { + ThemeService themeService = XDAT.getContextService().getBean(ThemeService.class); + String themedStyle = themeService.getThemePage("theme", "style"); + if (themedStyle != null) { + context.put("themedStyle", themedStyle); + } + String themedScript = themeService.getThemePage("theme", "script"); + if (themedScript != null) { + context.put("themedScript", themedScript); + } + } +} diff --git a/src/main/java/org/nrg/xnat/turbine/modules/screens/XDATScreen_themes.java b/src/main/java/org/nrg/xnat/turbine/modules/screens/XDATScreen_themes.java index 54957d5e..85a637e8 100644 --- a/src/main/java/org/nrg/xnat/turbine/modules/screens/XDATScreen_themes.java +++ b/src/main/java/org/nrg/xnat/turbine/modules/screens/XDATScreen_themes.java @@ -15,8 +15,9 @@ package org.nrg.xnat.turbine.modules.screens; import org.apache.log4j.Logger; import org.apache.turbine.util.RunData; import org.apache.velocity.context.Context; +import org.nrg.xdat.XDAT; import org.nrg.xdat.turbine.modules.screens.SecureScreen; -import org.nrg.xnat.restlet.extensions.ThemeRestlet; +import org.nrg.xnat.services.ThemeService; /** @@ -26,5 +27,14 @@ public class XDATScreen_themes extends SecureScreen { public final static Logger logger = Logger.getLogger(XDATScreen_themes.class); @Override protected void doBuildTemplate(RunData data, Context context) throws Exception { + ThemeService themeService = XDAT.getContextService().getBean(ThemeService.class); + String themedStyle = themeService.getThemePage("theme", "style"); + if (themedStyle != null) { + context.put("themedStyle", themedStyle); + } + String themedScript = themeService.getThemePage("theme", "script"); + if (themedScript != null) { + context.put("themedScript", themedScript); + } } } diff --git a/src/main/webapp/WEB-INF/conf/xnat-security.xml b/src/main/webapp/WEB-INF/conf/xnat-security.xml index a3bc84b9..408afbb2 100644 --- a/src/main/webapp/WEB-INF/conf/xnat-security.xml +++ b/src/main/webapp/WEB-INF/conf/xnat-security.xml @@ -122,6 +122,7 @@ <value>/images/**</value> <value>/scripts/**</value> <value>/style/**</value> + <value>/themes/**</value> <value>/applet/**</value> </list> </property> diff --git a/src/main/webapp/scripts/themeManagement.js b/src/main/webapp/scripts/themeManagement.js new file mode 100644 index 00000000..4e8d4992 --- /dev/null +++ b/src/main/webapp/scripts/themeManagement.js @@ -0,0 +1,123 @@ +/* + * org.nrg.xnat.turbine.modules.screens.ManageProtocol + * XNAT http://www.xnat.org + * Copyright (c) 2013, Washington University School of Medicine + * All Rights Reserved + * + * Released under the Simplified BSD. + * + * Author: Justin Cleveland <clevelandj@wustl.edu> + * Last modified 3/17/2016 3:33 PM + */ +var themeUrl = XNAT.url.rootUrl('xapi/theme'); +var s = '/', q = '?', a = '&'; +var csrf = 'XNAT_CSRF='+window.csrfToken; +$('#titleAppName').text(XNAT.app.siteId); +var currentTheme = $('#currentTheme'); +var themeSelector = $('#themeSelection'); +var uploadForm = document.getElementById('uploadThemeForm'); +var themeUploader = document.getElementById('themeFileUpload'); +var themeUploadSubmit = document.getElementById('submitThemeUploadButton'); +var selectedTheme = null; +function populateThemes(){ + getCurrentTheme(getAvailableThemes); +}; +function getCurrentTheme(callback){ + var role = 'global'; + $.get(themeUrl+s+role+q+csrf, null, function (data){ + themeSelector.empty(); + selectedTheme = data.name?data.name:'None'; + currentTheme.text(selectedTheme); + if(typeof callback === 'function'){ + callback(data.name); + } + }, 'json'); +}; +function getAvailableThemes(selected){ + $.get(themeUrl+q+csrf, null, function (data){ + themeSelector.empty(); + addThemeOptions(data, selected); + }, 'json'); +}; +function addThemeOptions(newThemeOptions, selected){ + if(Array.isArray(newThemeOptions)) { + $(newThemeOptions).each(function (i, opt) { + var select = ''; + if (selected == opt.value) { + select = ' selected="selected"'; + } + themeSelector.append('<option value="'+opt.value+'"'+select+'>'+opt.label+'</option>'); + }); + } +}; +function selectTheme(themeToSelect){ + if(themeToSelect && typeof themeToSelect === 'string'){ + themeSelector.val(themeToSelect); + } +}; +function setTheme(){ + xmodal.confirm({ + content: 'Theme selection appearances may not fully take effect until users log out, clear their browser cache and log back in.'+ + '<br><br>Are you sure you wish to change the global theme?', + action: function(){ + $.put(themeUrl+s+encodeURI(themeSelector.val())+q+csrf, null, function(data){ + console.log(data); + populateThemes() + }); + } + }); +}; +function removeTheme(){ + xmodal.confirm({ + content: 'Are you sure you wish to delete the selected theme?', + action: function(){ + $.delete(themeUrl+s+encodeURI(themeSelector.val())+q+csrf, null, function(data){ + console.log(data); + populateThemes(); + }); + } + }); +}; + +/*** Theme Package Upload Functions ***/ +uploadForm.action = themeUrl+q+csrf; +uploadForm.onsubmit = function(event) { + event.preventDefault(); + $(themeUploadSubmit).text('Uploading...'); + $(themeUploadSubmit).attr('disabled', 'disabled'); + var files = themeUploader.files; + var formData = new FormData(); + var uploaded = false; + for (var i = 0; i < files.length; i++) { + var file = files[i]; + if (!file.type.match('zip.*')) { + continue; + } + formData.append('themePackage', file, file.name); // formData.append('themes[]', file, file.name); + var xhr = new XMLHttpRequest(); + xhr.open('POST', uploadForm.action, true); + xhr.onload = function () { + if (xhr.status !== 200) { + console.log(xhr.statusText); + console.log(xhr.responseText); + xmodal.message('Upload Error', 'There was a problem uploading your theme package.<br>Server responded with: '+xhr.statusText); + } + $(themeUploadSubmit).text('Upload'); + $(themeUploadSubmit).removeAttr('disabled'); + var newThemeOptions = $.parseJSON(xhr.responseText); + var selected; + if(newThemeOptions[0]){ + selected = newThemeOptions[0].value; + } + addThemeOptions(newThemeOptions, selected); + }; + xhr.send(formData); + uploaded = true; + } + if(!uploaded){ + xmodal.message('Nothing Uploaded', 'No valid theme package files were selected for upload.'); + $(themeUploadSubmit).text('Upload'); + $(themeUploadSubmit).removeAttr('disabled'); + } +}; +$(populateThemes); // ...called once DOM is fully loaded "ready" diff --git a/src/main/webapp/xnat-templates/navigations/HeaderIncludes.vm b/src/main/webapp/xnat-templates/navigations/HeaderIncludes.vm index 9b6ebd7f..d7046f0b 100755 --- a/src/main/webapp/xnat-templates/navigations/HeaderIncludes.vm +++ b/src/main/webapp/xnat-templates/navigations/HeaderIncludes.vm @@ -15,6 +15,13 @@ <meta http-equiv="expires" content="-1"> <meta http-equiv="expires" content="Tue, 01 Jan 1980 1:00:00 GMT"> +#if ($themedRedirect) +<!-- As a backup, reload the appropriate page if a global theme style specifies a redirect --> +<script type="text/javascript"> + window.location = "$content.getURI("")$themedRedirect"; +</script> +#end + <!-- load polyfills before ANY other JavaScript --> <script type="text/javascript" src="$content.getURI('scripts/polyfills.js')"></script> @@ -286,4 +293,14 @@ <script type="text/javascript" src="$content.getURI('scripts/xnat/ui/popup.js')"></script> <script type="text/javascript" src="$content.getURI('scripts/xnat/ui/dialog.js')"></script> +#if ($themedScript) +<!-- Load active global theme script --> +<script type="text/javascript" src="$content.getURI("")$themedScript"></script> +#end + +#if ($themedStyle) +<!-- Load active global theme style --> +<link rel="stylesheet" type="text/css" href="$content.getURI("")$themedStyle"> +#end + #addGlobalCustomScreens("header") diff --git a/src/main/webapp/xnat-templates/screens/XDATScreen_themes.vm b/src/main/webapp/xnat-templates/screens/XDATScreen_themes.vm index f9d016ed..cf8dab12 100644 --- a/src/main/webapp/xnat-templates/screens/XDATScreen_themes.vm +++ b/src/main/webapp/xnat-templates/screens/XDATScreen_themes.vm @@ -25,88 +25,6 @@ </form> </div> -<script> - var themeUrl = "$content.getURI('/data/theme')"; - var q = '?', a = '&'; - var csrf = 'XNAT_CSRF='+window.csrfToken; - $('#titleAppName').text(XNAT.app.siteId); - var currentTheme = $('#currentTheme'); - var themeSelector = $('#themeSelection'); - var uploadForm = document.getElementById('uploadThemeForm'); - var themeUploader = document.getElementById('themeFileUpload'); - var themeUploadSubmit = document.getElementById('submitThemeUploadButton'); - var selectedTheme = null; - function populateThemes(themeToSelect){ - $.get(themeUrl+q+csrf, null, function (data){ - themeSelector.empty(); - $(data.themeOptions).each(function(i, opt) { - var selected = '', selectedTheme = null; - if(data.theme == opt.value){ - selectedTheme = data.theme; - selected = ' selected="selected"'; - currentTheme.text(selectedTheme?selectedTheme:'None'); - } - themeSelector.append('<option value="'+opt.value+'"'+selected+'>'+opt.label+'</option>'); - }); - if(themeToSelect && typeof themeToSelect === 'string'){ - themeSelector.val(themeToSelect); - } - }, 'json'); - } - function setTheme(){ - xmodal.confirm({ - content: 'Theme selection appearances may not fully take effect until users log out, clear their browser cache and log back in.'+ - '<br><br>Are you sure you wish to change the global theme?', - action: function(){ - $.put(themeUrl+q+'theme='+encodeURI(themeSelector.val())+a+csrf, null, populateThemes); - } - }); - } - function removeTheme(){ - xmodal.confirm({ - content: 'Are you sure you wish to delete the selected theme?', - action: function(){ - $.delete(themeUrl+q+'theme='+encodeURI(themeSelector.val())+a+csrf, null, populateThemes); - } - }); - } - - /*** Theme Package Upload Functions ***/ - uploadForm.action = themeUrl+q+csrf; - uploadForm.onsubmit = function(event) { - event.preventDefault(); - $(themeUploadSubmit).text('Uploading...'); - $(themeUploadSubmit).attr('disabled', 'disabled'); - var files = themeUploader.files; - var formData = new FormData(); - var uploaded = false; - for (var i = 0; i < files.length; i++) { - var file = files[i]; - if (!file.type.match('zip.*')) { - continue; - } - formData.append('themes[]', file, file.name); - var xhr = new XMLHttpRequest(); - xhr.open('POST', uploadForm.action, true); - xhr.onload = function () { - if (xhr.status !== 200) { - xmodal.message('Upload Error', xhr.responseText); - } - var uploadedTheme = xhr.responseText; - $(themeUploadSubmit).text('Upload'); - $(themeUploadSubmit).removeAttr('disabled'); - populateThemes(uploadedTheme); - }; - xhr.send(formData); - uploaded = true; - } - if(!uploaded){ - xmodal.message('Nothing Uploaded', 'No valid theme package files were selected for upload.'); - $(themeUploadSubmit).text('Upload'); - $(themeUploadSubmit).removeAttr('disabled'); - } - } - $(populateThemes); // ...called once DOM is fully loaded "ready" -</script> +<script type="text/javascript" src="$content.getURI('scripts/themeManagement.js')"></script> <!-- End XDATScreen_themes.vm --> \ No newline at end of file -- GitLab