/* * Copyright (C) 2004-2008 Jive Software, 2017-2026 Ignite Realtime Foundation. All rights reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.jivesoftware.admin; import org.dom4j.*; import org.jivesoftware.openfire.XMPPServer; import org.jivesoftware.util.ClassUtils; import org.jivesoftware.util.LocaleUtils; import org.jivesoftware.util.SAXReaderUtil; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.io.IOException; import java.io.InputStream; import java.net.URL; import java.util.*; /** * A model for admin tab and sidebar info. This class loads in XML definitions of the * data and produces an in-memory model.

* * This class loads its data from the {@code admin-sidebar.xml} file which is assumed * to be in the main application jar file. In addition, it will load files from * {@code META-INF/admin-sidebar.xml} if they're found. This allows developers to * extend the functionality of the admin console to provide more options. See the main * {@code admin-sidebar.xml} file for documentation of its format. */ public class AdminConsole { private static final Logger Log = LoggerFactory.getLogger(AdminConsole.class); private static Element coreModel; private static Map overrideModels; private static Element generatedModel; private static Properties gitprops; static { overrideModels = new LinkedHashMap<>(); gitprops = new Properties(); load(); } /** Not instantiatable */ private AdminConsole() { } /** * Adds XML stream to the tabs/sidebar model. * * @param name the name. * @param in the XML input stream. * @throws Exception if an error occurs when parsing the XML or adding it to the model. */ public static void addModel(String name, InputStream in) throws Exception { Document doc = SAXReaderUtil.readDocument(in); addModel(name, (Element)doc.selectSingleNode("/adminconsole")); } /** * Adds an <adminconsole> Element to the tabs/sidebar model. * * @param name the name. * @param element the Element * @throws Exception if an error occurs. */ public static synchronized void addModel(String name, Element element) throws Exception { overrideModels.put(name, element); rebuildModel(); } /** * Removes an <adminconsole> Element from the tabs/sidebar model. * * @param name the name. */ public static synchronized void removeModel(String name) { overrideModels.remove(name); rebuildModel(); } /** * Returns the name of the application. * * @return the name of the application. */ public static synchronized String getAppName() { Element appName = (Element)generatedModel.selectSingleNode("//adminconsole/global/appname"); if (appName != null) { String pluginName = appName.attributeValue("plugin"); return getAdminText(appName.getText(), pluginName); } else { return null; } } /** * Returns the URL of the main logo image for the admin console. * * @return the logo image. */ public static synchronized String getLogoImage() { Element globalLogoImage = (Element)generatedModel.selectSingleNode( "//adminconsole/global/logo-image"); if (globalLogoImage != null) { String pluginName = globalLogoImage.attributeValue("plugin"); return getAdminText(globalLogoImage.getText(), pluginName); } else { return null; } } /** * Returns the URL of the login image for the admin console. * * @return the login image. */ public static synchronized String getLoginLogoImage() { Element globalLoginLogoImage = (Element)generatedModel.selectSingleNode( "//adminconsole/global/login-image"); if (globalLoginLogoImage != null) { String pluginName = globalLoginLogoImage.attributeValue("plugin"); return getAdminText(globalLoginLogoImage.getText(), pluginName); } else { return null; } } /** * Returns the version string displayed in the admin console. * * @return the version string. */ public static synchronized String getVersionString() { Element globalVersion = (Element)generatedModel.selectSingleNode( "//adminconsole/global/version"); if (globalVersion != null) { String pluginName = globalVersion.attributeValue("plugin"); return getAdminText(globalVersion.getText(), pluginName); } else { // Default to the Openfire version if none has been provided via XML. XMPPServer xmppServer = XMPPServer.getInstance(); return xmppServer.getServerInfo().getVersion().getVersionString(); } } /** * Returns the git SHA of the commit at which this version was built * * @return the git SHA. */ public static synchronized String getGitSHAString() { String shaString = gitprops.getProperty("git.commit.id.abbrev"); if (shaString == null){ shaString = "unknown"; } return shaString; } /** * Returns the model. The model should be considered read-only. * * @return the model. */ public static synchronized Element getModel() { return generatedModel; } /** * Convenience method to select an element from the model by its ID. If an * element with a matching ID is not found, {@code null} will be returned. * * @param id the ID. * @return the element. */ public static synchronized Element getElemnetByID(String id) { return (Element)generatedModel.selectSingleNode("//*[@id='" + id + "']"); } /** * Returns a text element for the admin console, applying the appropriate locale. * Internationalization logic will only be applied if the String is specially encoded * in the format "${key.name}". If it is, the String is pulled from the resource bundle. * If the pluginName is not {@code null}, the plugin's resource bundle will be used * to look up the key. * * @param string the String. * @param pluginName the name of the plugin that the i18n String can be found in, * or {@code null} if the standard Openfire resource bundle should be used. * @return the string, or if the string is encoded as an i18n key, the value from * the appropriate resource bundle. */ public static String getAdminText(String string, String pluginName) { if (string == null) { return null; } // Look for the key symbol: if (string.indexOf("${") == 0 && string.indexOf("}") == string.length()-1) { return LocaleUtils.getLocalizedString(string.substring(2, string.length()-1), pluginName); } return string; } private static void load() { // Load the core model as the admin-sidebar.xml file from the classpath. InputStream in = ClassUtils.getResourceAsStream("/admin-sidebar.xml"); if (in == null) { Log.error("Failed to load admin-sidebar.xml file from Openfire classes - admin " + "console will not work correctly."); return; } try { Document doc = SAXReaderUtil.readDocument(in); coreModel = (Element)doc.selectSingleNode("/adminconsole"); } catch (Exception e) { Log.error("Failure when parsing main admin-sidebar.xml file", e); if (e instanceof InterruptedException) { Thread.currentThread().interrupt(); } } try { in.close(); } catch (Exception ex) { Log.debug("An exception occurred while trying to close the input stream that was used to read admin-sidebar.xml", ex); } // Load other admin-sidebar.xml files from the classpath, and load git properties ClassLoader[] classLoaders = getClassLoaders(); for (ClassLoader classLoader : classLoaders) { URL url = null; try { if (classLoader != null) { Enumeration e = classLoader.getResources("/META-INF/admin-sidebar.xml"); while (e.hasMoreElements()) { url = e.nextElement(); try { in = url.openStream(); addModel("admin", in); } finally { try { if (in != null) { in.close(); } } catch (Exception ex) { Log.debug("An exception occurred while trying to close the input stream that was used to read admin-sidebar.xml", ex); } } } } } catch (Exception e) { String msg = "Failed to load admin-sidebar.xml"; if (url != null) { msg += " from resource: " + url.toString(); } Log.warn(msg, e); if (e instanceof InterruptedException) { Thread.currentThread().interrupt(); } } try { if(classLoader != null && gitprops.isEmpty()){ InputStream inputStream = classLoader.getResourceAsStream("git.properties"); if(inputStream != null){ try { gitprops.load(inputStream); } catch (IOException e) { e.printStackTrace(); } } } } catch (Exception e){ String msg = "Failed to load git properties"; Log.warn(msg, e); } } rebuildModel(); } /** * Rebuilds the generated model. */ private static synchronized void rebuildModel() { Document doc = DocumentFactory.getInstance().createDocument(); generatedModel = coreModel.createCopy(); doc.add(generatedModel); // Add in all overrides. for (Element element : overrideModels.values()) { // See if global settings are overridden. Element appName = (Element)element.selectSingleNode("//adminconsole/global/appname"); if (appName != null) { Element existingAppName = (Element)generatedModel.selectSingleNode( "//adminconsole/global/appname"); existingAppName.setText(appName.getText()); if (appName.attributeValue("plugin") != null) { existingAppName.addAttribute("plugin", appName.attributeValue("plugin")); } } Element appLogoImage = (Element)element.selectSingleNode("//adminconsole/global/logo-image"); if (appLogoImage != null) { Element existingLogoImage = (Element)generatedModel.selectSingleNode( "//adminconsole/global/logo-image"); if (existingLogoImage == null) { existingLogoImage = ((Element)generatedModel.selectSingleNode("//adminconsole/global")) .addElement("logo-image"); } existingLogoImage.setText(appLogoImage.getText()); if (appLogoImage.attributeValue("plugin") != null) { existingLogoImage.addAttribute("plugin", appLogoImage.attributeValue("plugin")); } } Element appLoginImage = (Element)element.selectSingleNode("//adminconsole/global/login-image"); if (appLoginImage != null) { Element existingLoginImage = (Element)generatedModel.selectSingleNode( "//adminconsole/global/login-image"); if (existingLoginImage == null) { existingLoginImage = ((Element)generatedModel.selectSingleNode("//adminconsole/global")) .addElement("login-image"); } existingLoginImage.setText(appLoginImage.getText()); if (appLoginImage.attributeValue("plugin") != null) { existingLoginImage.addAttribute("plugin", appLoginImage.attributeValue("plugin")); } } Element appVersion = (Element)element.selectSingleNode("//adminconsole/global/version"); if (appVersion != null) { Element existingVersion = (Element)generatedModel.selectSingleNode( "//adminconsole/global/version"); if (existingVersion != null) { existingVersion.setText(appVersion.getText()); if (appVersion.attributeValue("plugin") != null) { existingVersion.addAttribute("plugin", appVersion.attributeValue("plugin")); } } else { ((Element)generatedModel.selectSingleNode( "//adminconsole/global")).add(appVersion.createCopy()); } } // Tabs for (Object o : element.selectNodes("//tab")) { Element tab = (Element) o; String id = tab.attributeValue("id"); Element existingTab = getElemnetByID(id); // Simple case, there is no existing tab with the same id. if (existingTab == null) { generatedModel.add(tab.createCopy()); } // More complex case -- a tab with the same id already exists. // In this case, we have to overrite only the difference between // the two elements. else { overrideEntry(existingTab, tab); } } } // OF-1484: Order everything explicitly. orderModel(); // Only after explicit ordering is applied (OF-3168), make sure that the URL on every tab is set. If not, // default to the url of the first item. for (Object o : generatedModel.selectNodes("//adminconsole//tab")) { Element tab = (Element) o; if (tab.attributeValue("url") == null) { final List sidebars = tab.elements("sidebar"); if (!sidebars.isEmpty()) { sidebars.sort(new ElementByOrderAttributeComparator()); final List items = sidebars.get(0).elements("item"); if (!items.isEmpty()) { items.sort(new ElementByOrderAttributeComparator()); tab.addAttribute("url", items.get(0).attributeValue("url")); } } } } } /** * Sorts all tabs, their containing sidebars, and their containing entries based on the value of their 'order' * attributes. */ private static void orderModel() { final Visitor visitor = new VisitorSupport() { @Override public void visit( Element node ) { // This orders only the elements from the content, which can get messy if mixed content is of importance. // At the time of writing, the content other than elements was whitespace text (for indentation), which // is safe to ignore. node.content().sort(new ElementByOrderAttributeComparator()); super.visit( node ); } }; generatedModel.accept( visitor ); } private static void overrideSidebar(Element sidebar, Element overrideSidebar) { // Override name. overrideCommonAttributes(sidebar, overrideSidebar); // Override entries. for (Iterator i=overrideSidebar.elementIterator(); i.hasNext(); ) { Element entry = i.next(); String id = entry.attributeValue("id"); Element existingEntry = getElemnetByID(id); // Simple case, there is no existing sidebar with the same id. if (existingEntry == null) { sidebar.add(entry.createCopy()); } // More complex case -- an entry with the same id already exists. // In this case, we have to overrite only the difference between // the two elements. else { overrideEntry(existingEntry, entry); } } } private static void overrideEntry(Element entry, Element overrideEntry) { // Override name. overrideCommonAttributes(entry, overrideEntry); if (overrideEntry.attributeValue("url") != null) { entry.addAttribute("url", overrideEntry.attributeValue("url")); } if (overrideEntry.attributeValue("description") != null) { entry.addAttribute("description", overrideEntry.attributeValue("description")); } // Override any sidebars contained in the entry. for (Iterator i = overrideEntry.elementIterator(); i.hasNext(); ) { Element sidebar = i.next(); String id = sidebar.attributeValue("id"); Element existingSidebar = getElemnetByID(id); // Simple case, there is no existing sidebar with the same id. if (existingSidebar == null) { entry.add(sidebar.createCopy()); } // More complex case -- a sidebar with the same id already exists. // In this case, we have to overrite only the difference between // the two elements. else { overrideSidebar(existingSidebar, sidebar); } } } private static void overrideCommonAttributes(Element entry, Element overrideEntry) { if (overrideEntry.attributeValue("name") != null) { entry.addAttribute("name", overrideEntry.attributeValue("name")); } if (overrideEntry.attributeValue("plugin") != null) { entry.addAttribute("plugin", overrideEntry.attributeValue("plugin")); } if (overrideEntry.attributeValue("order") != null) { entry.addAttribute("order", overrideEntry.attributeValue("order")); } } /** * Returns an array of class loaders to load resources from. * * @return an array of class loaders to load resources from. */ private static ClassLoader[] getClassLoaders() { ClassLoader[] classLoaders = new ClassLoader[3]; classLoaders[0] = AdminConsole.class.getClass().getClassLoader(); classLoaders[1] = Thread.currentThread().getContextClassLoader(); classLoaders[2] = ClassLoader.getSystemClassLoader(); return classLoaders; } /** * A comparator that compares Nodes by the value of their 'order' attribute, if the node is an Element. When it is * not, or when the 'order' attribute is absent, or cannot be parsed as an integer, the value '0' is used. * * @author Guus der Kinderen, guus.der.kinderen@gmail.com */ private static class ElementByOrderAttributeComparator implements Comparator { @Override public int compare( Node o1, Node o2 ) { try { final int p1 = o1 instanceof Element ? Integer.parseInt( ((Element)o1).attributeValue( "order", "0" ) ) : 0; final int p2 = o2 instanceof Element ? Integer.parseInt( ((Element)o2).attributeValue( "order", "0" ) ) : 0; return Integer.compare( p1, p2 ); } catch ( NumberFormatException e ) { Log.warn( "Unable to sort admin console tabs, as a non-numeric 'order' attribute value was found.", e ); return 0; } } } }