package controllers; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; import com.github.jknack.handlebars.Handlebars; import com.github.jknack.handlebars.Helper; import com.github.jknack.handlebars.MarkdownHelper; import com.github.jknack.handlebars.Options; import com.github.jknack.handlebars.Template; import com.github.jknack.handlebars.helper.StringHelpers; import com.github.jknack.handlebars.io.TemplateLoader; import com.maxmind.geoip2.DatabaseReader; import helpers.HandlebarsHelpers; import helpers.JSONForm; import helpers.ResourceTemplateLoader; import helpers.UniversalFunctions; import models.Resource; import models.TripleCommit; import org.apache.commons.io.IOUtils; import org.apache.commons.lang3.StringUtils; import play.Configuration; import play.Environment; import play.Logger; import play.i18n.Lang; import play.mvc.Controller; import play.twirl.api.Html; import services.AccountService; import services.AggregationProvider; import services.QueryContext; import services.repository.BaseRepository; import services.repository.ElasticsearchRepository; import javax.inject.Inject; import java.io.File; import java.io.FileInputStream; import java.io.IOException; import java.io.InputStream; import java.io.UnsupportedEncodingException; import java.net.InetAddress; import java.net.URISyntaxException; import java.net.URL; import java.net.URLDecoder; import java.time.ZonedDateTime; import java.time.format.DateTimeFormatter; import java.util.ArrayList; import java.util.Base64; import java.util.Enumeration; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Locale; import java.util.Map; import java.util.ResourceBundle; import java.util.Set; import java.util.jar.JarEntry; import java.util.jar.JarFile; /** * @author fo */ //FIXME: re-enable Authorized.class, currently solved by getHttpBasicAuthUser() // see https://github.com/playframework/playframework/issues/4706 //@With(Authorized.class) public abstract class OERWorldMap extends Controller { Configuration mConf; Environment mEnv; protected static BaseRepository mBaseRepository = null; static AccountService mAccountService; static DatabaseReader mLocationLookup; private static synchronized void createBaseRepository(Configuration aConf) { if (mBaseRepository == null) { try { mBaseRepository = new BaseRepository(aConf.underlying(), new ElasticsearchRepository(aConf.underlying())); } catch (final Exception ex) { throw new RuntimeException("Failed to create Respository", ex); } } } private static synchronized void createAccountService(Configuration aConf) { if (mAccountService == null) { mAccountService = new AccountService( new File(aConf.getString("user.token.dir")), new File(aConf.getString("ht.passwd")), new File(aConf.getString("ht.groups")), new File(aConf.getString("ht.profiles")), new File(aConf.getString("ht.permissions"))); mAccountService.setApache2Ctl(aConf.getString("ht.apache2ctl.restart")); } } private static synchronized void createLocationLookup(Environment aEnv) { if (mLocationLookup == null) { try { mLocationLookup = new DatabaseReader.Builder(aEnv.resourceAsStream("GeoLite2-Country.mmdb")).build(); } catch (final IOException ex) { throw new RuntimeException("Failed to create location lookup", ex); } } } @Inject public OERWorldMap(Configuration aConf, Environment aEnv) { mConf = aConf; mEnv = aEnv; // Repository createBaseRepository(mConf); // Account service createAccountService(mConf); // Location lookup createLocationLookup(mEnv); } boolean getEmbed() { return ctx().request().queryString().containsKey("embed"); } Locale getLocale() { Locale locale = new Locale("en", "US"); if (mConf.getBoolean("i18n.enabled")) { List<Lang> acceptLanguages = request().acceptLanguages(); if (acceptLanguages.size() > 0) { locale = acceptLanguages.get(0).toLocale(); } } return locale; } Resource getUser() { Resource user = null; Logger.trace("Username " + getHttpBasicAuthUser()); String profileId = mAccountService.getProfileId(getHttpBasicAuthUser()); if (!StringUtils.isEmpty(profileId)) { user = getRepository().getResource(profileId); } return user; } String getHttpBasicAuthUser() { String authHeader = ctx().request().getHeader(AUTHORIZATION); if (null == authHeader) { return null; } String auth = authHeader.substring(6); byte[] decoded = Base64.getDecoder().decode(auth); String[] credentials; try { credentials = new String(decoded, "UTF-8").split(":"); } catch (UnsupportedEncodingException e) { e.printStackTrace(); return null; } if (credentials.length != 2) { return null; } return credentials[0]; } QueryContext getQueryContext() { List<String> roles = new ArrayList<>(); roles.add("guest"); if (getUser() != null) { roles.add("authenticated"); } return new QueryContext(roles); } ResourceBundle getMessages() { return ResourceBundle.getBundle("messages", getLocale()); } ResourceBundle getEmails() { return ResourceBundle.getBundle("emails", getLocale()); } String getLocation() { try { return mLocationLookup.country(InetAddress.getByName(request().remoteAddress())).getCountry().getIsoCode(); } catch (Exception ex) { Logger.trace("Could not read host", ex); return "GB"; } } //TODO: is this right here? how else to implement? public String getLabel(String aId) { Resource resource = mBaseRepository.getResource(aId); if (null == resource) { return aId; } Object name = resource.get("name"); if (name instanceof ArrayList) { // Return requested language for (Object n : ((ArrayList) name)) { if (n instanceof Resource) { String language = ((Resource) n).getAsString("@language"); if (language.equals(getLocale().getLanguage())) { return ((Resource) n).getAsString("@value"); } } } // Return English if requested language is not available for (Object n : ((ArrayList) name)) { if (n instanceof Resource) { String language = ((Resource) n).getAsString("@language"); if (language.equals("en")) { return ((Resource) n).getAsString("@value"); } } } // Return first available if English is not available for (Object n : ((ArrayList) name)) { if (n instanceof Resource) { return ((Resource) n).getAsString("@value"); } } } return aId; } public Resource getUser(String aId) { Resource user = null; String profileId = mAccountService.getProfileId(aId); if (!StringUtils.isEmpty(profileId)) { user = getRepository().getResource(profileId); } return user; } protected Html render(String pageTitle, String templatePath, Map<String, Object> scope, List<Map<String, Object>> messages) { Map<String, Object> mustacheData = new HashMap<>(); mustacheData.put("scope", scope); mustacheData.put("messages", messages); mustacheData.put("user", getUser()); mustacheData.put("username", getHttpBasicAuthUser()); mustacheData.put("pageTitle", pageTitle); mustacheData.put("template", templatePath); mustacheData.put("config", mConf.asMap()); mustacheData.put("templates", getClientTemplates()); mustacheData.put("language", getLocale().toLanguageTag()); mustacheData.put("requestUri", mConf.getString("proxy.host").concat(request().uri())); mustacheData.put("userLocation", getLocation()); mustacheData.put("embed", getEmbed()); Map<String, Object> skos = new HashMap<>(); try { skos.put("esc", new ObjectMapper().readValue(mEnv.classLoader().getResourceAsStream("public/json/esc.json"), HashMap.class)); skos.put("isced", new ObjectMapper().readValue(mEnv.classLoader().getResourceAsStream("public/json/isced-1997.json"), HashMap.class)); } catch (IOException e) { Logger.warn("Could not read SKOS file", e); } mustacheData.put("skos", skos); try { if (scope != null) { Resource globalAggregation = mBaseRepository.aggregate(AggregationProvider.getByCountryAggregation(0)); Resource keywordAggregation = mBaseRepository.aggregate(AggregationProvider.getKeywordsAggregation(0)); scope.put("globalAggregation", globalAggregation); scope.put("keywordAggregation", keywordAggregation); } } catch (IOException e) { Logger.warn("Could not add global statistics", e); } boolean mayAdd = (getUser() != null); boolean mayAdminister = (getUser() != null) && mAccountService.getGroups(getHttpBasicAuthUser()).contains("admin"); Map<String, Object> permissions = new HashMap<>(); permissions.put("add", mayAdd); permissions.put("administer", mayAdminister); mustacheData.put("permissions", permissions); TemplateLoader loader = new ResourceTemplateLoader(mEnv.classLoader()); loader.setPrefix("public/mustache"); loader.setSuffix(""); Handlebars handlebars = new Handlebars(loader); handlebars.infiniteLoops(true); handlebars.registerHelpers(StringHelpers.class); handlebars.registerHelper("obfuscate", new Helper<String>() { public CharSequence apply(String string, Options options) { return UniversalFunctions.getHtmlEntities(string); } }); try { handlebars.registerHelpers(new File("public/javascripts/helpers.js")); } catch (Exception e) { Logger.error("Could not register helpers", e); } HandlebarsHelpers.setController(this); handlebars.registerHelpers(new HandlebarsHelpers()); try { handlebars.registerHelpers(new File("public/javascripts/helpers/shared.js")); } catch (Exception e) { Logger.error("Could not register helpers", e); } try { handlebars.registerHelpers(new File("public/javascripts/handlebars.form-helpers.js")); } catch (Exception e) { Logger.error("Could not register helpers", e); } try { handlebars.registerHelpers(new File("public/vendor/moment/handlebars.moment.js")); } catch (Exception e) { Logger.error("Could not register helpers", e); } handlebars.registerHelper("md", new MarkdownHelper()); try { Template template = handlebars.compile("main.mustache"); return Html.apply(template.apply(mustacheData)); } catch (IOException e) { Logger.error("Could not compile template", e); return null; } } protected Html render(String pageTitle, String templatePath, Map<String, Object> scope) { return render(pageTitle, templatePath, scope, null); } protected Html render(String pageTitle, String templatePath) { return render(pageTitle, templatePath, null, null); } protected BaseRepository getRepository() { return mBaseRepository; } protected AccountService getAccountService() { return mAccountService; } /** * Get resource from JSON body or form data * @return The JSON node */ protected JsonNode getJsonFromRequest() { JsonNode jsonNode = ctx().request().body().asJson(); if (jsonNode == null) { Map<String, String[]> formUrlEncoded = ctx().request().body().asFormUrlEncoded(); if (formUrlEncoded != null) { jsonNode = JSONForm.parseFormData(formUrlEncoded, true); } } return jsonNode; } /** * Get metadata suitable for record provinence * * @return Map containing current getUser() in author and current time in date field. */ protected Map<String, String> getMetadata() { Map<String, String> metadata = new HashMap<>(); if (!StringUtils.isEmpty(getHttpBasicAuthUser())) { metadata.put(TripleCommit.Header.AUTHOR_HEADER, getHttpBasicAuthUser()); } else { metadata.put(TripleCommit.Header.AUTHOR_HEADER, "System"); } metadata.put(TripleCommit.Header.DATE_HEADER, ZonedDateTime.now().format(DateTimeFormatter.ISO_OFFSET_DATE_TIME)); return metadata; } private String getClientTemplates() { final List<String> templates = new ArrayList<>(); final String dir = "public/mustache/ClientTemplates/"; final ClassLoader classLoader = mEnv.classLoader(); String[] paths = new String[0]; try { paths = getResourceListing(dir, classLoader); } catch (URISyntaxException | IOException e) { Logger.error("Could not find client templates", e); } for (String path : paths) { try { String template = "<script id=\"".concat(path).concat("\" type=\"text/mustache\">\n"); InputStream templateStream = classLoader.getResourceAsStream(dir + path); if (templateStream == null){ templateStream = new FileInputStream(dir + path); } template = template.concat(IOUtils.toString(templateStream)); templateStream.close(); template = template.concat("</script>\n\n"); templates.add(template); } catch (IOException e) { Logger.error("Could not load client template", e); } } return String.join("\n", templates); } /** * List directory contents for a resource folder. Not recursive. This is * basically a brute-force implementation. Works for regular files and also * JARs. * * Adapted from http://www.uofr.net/~greg/java/get-resource-listing.html * * @param path * Should end with "/", but not start with one. * @return Just the name of each member item, not the full paths. * @throws URISyntaxException * @throws IOException */ private String[] getResourceListing(String path, ClassLoader classLoader) throws URISyntaxException, IOException { URL dirURL = classLoader.getResource(path); if (dirURL == null) { return mEnv.getFile(path).list(); } else if (dirURL.getProtocol().equals("jar")) { String jarPath = dirURL.getPath().substring(5, dirURL.getPath().indexOf("!")); // strip // out // only // the // JAR // file JarFile jar = new JarFile(URLDecoder.decode(jarPath, "UTF-8")); Enumeration<JarEntry> entries = jar.entries(); // gives ALL entries in jar Set<String> result = new HashSet<>(); // avoid duplicates in case it // is a subdirectory while (entries.hasMoreElements()) { String name = entries.nextElement().getName(); if (name.startsWith(path)) { // filter according to the path String entry = name.substring(path.length()); int checkSubdir = entry.indexOf("/"); if (checkSubdir >= 0) { // if it is a subdirectory, we just return the directory name entry = entry.substring(0, checkSubdir); } if (!entry.equals("")) { result.add(entry); } } } jar.close(); return result.toArray(new String[result.size()]); } else if (dirURL.getProtocol().equals("file")) { return new File(dirURL.getFile()).list(); } throw new UnsupportedOperationException("Cannot list files for URL " + dirURL); } }