View Javadoc
1   /*
2    * This file is part of ***  M y C o R e  ***
3    * See http://www.mycore.de/ for details.
4    *
5    * MyCoRe is free software: you can redistribute it and/or modify
6    * it under the terms of the GNU General Public License as published by
7    * the Free Software Foundation, either version 3 of the License, or
8    * (at your option) any later version.
9    *
10   * MyCoRe is distributed in the hope that it will be useful,
11   * but WITHOUT ANY WARRANTY; without even the implied warranty of
12   * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13   * GNU General Public License for more details.
14   *
15   * You should have received a copy of the GNU General Public License
16   * along with MyCoRe.  If not, see <http://www.gnu.org/licenses/>.
17   */
18  
19  package org.mycore.frontend;
20  
21  import static org.mycore.access.MCRAccessManager.PERMISSION_READ;
22  
23  import java.io.ByteArrayOutputStream;
24  import java.io.File;
25  import java.io.IOException;
26  import java.io.UnsupportedEncodingException;
27  import java.net.MalformedURLException;
28  import java.net.URL;
29  import java.net.URLConnection;
30  import java.util.ArrayList;
31  import java.util.HashMap;
32  import java.util.List;
33  import java.util.concurrent.Executor;
34  import java.util.concurrent.Executors;
35  import java.util.concurrent.TimeUnit;
36  
37  import javax.xml.transform.OutputKeys;
38  import javax.xml.transform.Transformer;
39  import javax.xml.transform.TransformerException;
40  import javax.xml.transform.TransformerFactory;
41  import javax.xml.transform.TransformerFactoryConfigurationError;
42  import javax.xml.transform.dom.DOMSource;
43  import javax.xml.transform.stream.StreamResult;
44  import javax.xml.xpath.XPath;
45  import javax.xml.xpath.XPathConstants;
46  import javax.xml.xpath.XPathExpressionException;
47  
48  import org.apache.logging.log4j.LogManager;
49  import org.apache.logging.log4j.Logger;
50  import org.jdom2.Attribute;
51  import org.jdom2.Document;
52  import org.jdom2.Element;
53  import org.jdom2.JDOMException;
54  import org.jdom2.filter.Filters;
55  import org.jdom2.input.SAXBuilder;
56  import org.jdom2.input.sax.XMLReaders;
57  import org.jdom2.output.DOMOutputter;
58  import org.jdom2.output.support.AbstractDOMOutputProcessor;
59  import org.jdom2.output.support.FormatStack;
60  import org.jdom2.util.NamespaceStack;
61  import org.jdom2.xpath.XPathExpression;
62  import org.jdom2.xpath.XPathFactory;
63  import org.mycore.access.MCRAccessInterface;
64  import org.mycore.access.MCRAccessManager;
65  import org.mycore.access.MCRRuleAccessInterface;
66  import org.mycore.access.mcrimpl.MCRAccessStore;
67  import org.mycore.common.MCRException;
68  import org.mycore.common.MCRSessionMgr;
69  import org.mycore.common.config.MCRConfiguration2;
70  import org.mycore.common.xml.MCRURIResolver;
71  import org.w3c.dom.Node;
72  import org.w3c.dom.NodeList;
73  
74  import com.google.common.cache.CacheBuilder;
75  import com.google.common.cache.CacheLoader;
76  import com.google.common.cache.LoadingCache;
77  import com.google.common.util.concurrent.Futures;
78  import com.google.common.util.concurrent.ListenableFuture;
79  import com.google.common.util.concurrent.ListenableFutureTask;
80  
81  import jakarta.servlet.ServletContext;
82  
83  /**
84   *
85   * Xalan extention for navigation.xsl
86   *
87   */
88  public class MCRLayoutUtilities {
89      // strategies for access verification
90      public static final int ALLTRUE = 1;
91  
92      public static final int ONETRUE_ALLTRUE = 2;
93  
94      public static final int ALL2BLOCKER_TRUE = 3;
95  
96      public static final String NAV_RESOURCE = MCRConfiguration2.getString("MCR.NavigationFile")
97          .orElse("/config/navigation.xml");
98  
99      static final String OBJIDPREFIX_WEBPAGE = "webpage:";
100 
101     private static final int STANDARD_CACHE_SECONDS = 10;
102 
103     private static final XPathFactory XPATH_FACTORY = XPathFactory.instance();
104 
105     private static final Logger LOGGER = LogManager.getLogger(MCRLayoutUtilities.class);
106 
107     private static final ServletContext SERVLET_CONTEXT = MCRURIResolver.getServletContext();
108 
109     private static final boolean ACCESS_CONTROLL_ON = MCRConfiguration2
110         .getOrThrow("MCR.Website.ReadAccessVerification", Boolean::parseBoolean);
111 
112     private static HashMap<String, Element> itemStore = new HashMap<>();
113 
114     private static final LoadingCache<String, DocumentHolder> NAV_DOCUMENT_CACHE = CacheBuilder.newBuilder()
115         .refreshAfterWrite(STANDARD_CACHE_SECONDS, TimeUnit.SECONDS).build(new CacheLoader<String, DocumentHolder>() {
116 
117             Executor executor = Executors.newSingleThreadExecutor(r -> new Thread(r, "navigation.xml refresh"));
118 
119             @Override
120             public DocumentHolder load(String key) throws Exception {
121                 URL url = SERVLET_CONTEXT.getResource(key);
122                 try {
123                     return new DocumentHolder(url);
124                 } finally {
125                     itemStore.clear();
126                 }
127 
128             }
129 
130             @Override
131             public ListenableFuture<DocumentHolder> reload(final String key, DocumentHolder oldValue) throws Exception {
132                 URL url = SERVLET_CONTEXT.getResource(key);
133                 if (oldValue.isValid(url)) {
134                     LOGGER.debug("Keeping {} in cache", url);
135                     return Futures.immediateFuture(oldValue);
136                 }
137                 ListenableFutureTask<DocumentHolder> task = ListenableFutureTask.create(() -> load(key));
138                 executor.execute(task);
139                 return task;
140             }
141         });
142 
143     /**
144      * Verifies a given $webpage-ID (//item/@href) from navigation.xml on read
145      * permission, based on ACL-System. To be used by XSL with
146      * Xalan-Java-Extension-Call. $blockerWebpageID can be used as already
147      * verified item with read access. So, only items of the ancestor axis till
148      * and exclusive $blockerWebpageID are verified. Use this, if you want to
149      * speed up the check
150      *
151      * @param webpageID
152      *            any item/@href from navigation.xml
153      * @param blockerWebpageID
154      *            any ancestor item of webpageID from navigation.xml
155      * @return true if access granted, false if not
156      */
157     public static boolean readAccess(String webpageID, String blockerWebpageID) {
158         if (ACCESS_CONTROLL_ON) {
159             long startTime = System.currentTimeMillis();
160             boolean access = getAccess(webpageID, PERMISSION_READ, ALL2BLOCKER_TRUE, blockerWebpageID);
161             LOGGER.debug("checked read access for webpageID= {} (with blockerWebpageID ={}) => {}: took {} msec.",
162                 webpageID, blockerWebpageID, access, getDuration(startTime));
163             return access;
164         } else {
165             return true;
166         }
167     }
168 
169     /**
170      * Verifies a given $webpage-ID (//item/@href) from navigation.xml on read
171      * permission, based on ACL-System. To be used by XSL with
172      * Xalan-Java-Extension-Call.
173      *
174      * @param webpageID
175      *            any item/@href from navigation.xml
176      * @return true if access granted, false if not
177      */
178     public static boolean readAccess(String webpageID) {
179         if (ACCESS_CONTROLL_ON) {
180             long startTime = System.currentTimeMillis();
181             boolean access = getAccess(webpageID, PERMISSION_READ, ALLTRUE);
182             LOGGER.debug("checked read access for webpageID= {} => {}: took {} msec.", webpageID, access,
183                 getDuration(startTime));
184             return access;
185         } else {
186             return true;
187         }
188     }
189 
190     /**
191      * Returns all labels of the ancestor axis for the given item within
192      * navigation.xml
193      *
194      * @param item a navigation item
195      * @return Label as String, like "labelRoot &gt; labelChild &gt;
196      *         labelChildOfChild"
197      */
198     public static String getAncestorLabels(Element item) {
199         StringBuilder label = new StringBuilder();
200         String lang = MCRSessionMgr.getCurrentSession().getCurrentLanguage().trim();
201         XPathExpression<Element> xpath;
202         Element ic = null;
203         xpath = XPATH_FACTORY.compile("//.[@href='" + getWebpageID(item) + "']", Filters.element());
204         ic = xpath.evaluateFirst(getNavi());
205         while (ic.getName().equals("item")) {
206             ic = ic.getParentElement();
207             String webpageID = getWebpageID(ic);
208             Element labelEl = null;
209             xpath = XPATH_FACTORY.compile("//.[@href='" + webpageID + "']/label[@xml:lang='" + lang + "']",
210                 Filters.element());
211             labelEl = xpath.evaluateFirst(getNavi());
212             if (labelEl != null) {
213                 if (label.length() == 0) {
214                     label = new StringBuilder(labelEl.getTextTrim());
215                 } else {
216                     label.insert(0, labelEl.getTextTrim() + " > ");
217                 }
218             }
219         }
220         return label.toString();
221     }
222 
223     /**
224      * Verifies, if an item of navigation.xml has a given $permission.
225      *
226      * @param webpageID
227      *            item/@href
228      * @param permission
229      *            permission to look for
230      * @param strategy
231      *            ALLTRUE =&gt; all ancestor items of webpageID must have the
232      *            permission, ONETRUE_ALLTRUE =&gt; only 1 ancestor item must have
233      *            the permission
234      * @return true, if access, false if no access
235      */
236     public static boolean getAccess(String webpageID, String permission, int strategy) {
237         Element item = getItem(webpageID);
238         // check permission according to $strategy
239         boolean access = strategy == ALLTRUE;
240         if (strategy == ALLTRUE) {
241             while (item != null && access) {
242                 access = itemAccess(permission, item, access);
243                 item = item.getParentElement();
244             }
245         } else if (strategy == ONETRUE_ALLTRUE) {
246             while (item != null && !access) {
247                 access = itemAccess(permission, item, access);
248                 item = item.getParentElement();
249             }
250         }
251         return access;
252     }
253 
254     /**
255      * Verifies, if an item of navigation.xml has a given $permission with a
256      * stop item ($blockerWebpageID)
257      *
258      * @param webpageID
259      *            item/@href
260      * @param permission
261      *            permission to look for
262      * @param strategy
263      *            ALL2BLOCKER_TRUE =&gt; all ancestor items of webpageID till and
264      *            exlusiv $blockerWebpageID must have the permission
265      * @param blockerWebpageID
266      *            any ancestor item of webpageID from navigation.xml
267      * @return true, if access, false if no access
268      */
269     public static boolean getAccess(String webpageID, String permission, int strategy, String blockerWebpageID) {
270         Element item = getItem(webpageID);
271         // check permission according to $strategy
272         boolean access = false;
273         if (strategy == ALL2BLOCKER_TRUE) {
274             access = true;
275             String itemHref;
276             do {
277                 access = itemAccess(permission, item, access);
278                 item = item.getParentElement();
279                 itemHref = getWebpageID(item);
280             } while (item != null && access && !(itemHref != null && itemHref.equals(blockerWebpageID)));
281         }
282         return access;
283     }
284 
285     /**
286      * Returns a Element presentation of an item[@href=$webpageID]
287      *
288      * @param webpageID
289      * @return Element
290      */
291     private static Element getItem(String webpageID) {
292         Element item = itemStore.get(webpageID);
293         if (item == null) {
294             XPathExpression<Element> xpath = XPATH_FACTORY.compile("//.[@href='" + webpageID + "']", Filters.element());
295             item = xpath.evaluateFirst(getNavi());
296             itemStore.put(webpageID, item);
297         }
298         return item;
299     }
300 
301     /**
302      * Verifies a single item on access according to $permission Falls back to version without query
303      * if no rule for exact query string exists.
304      *
305      * @param permission an ACL permission
306      * @param item element to check
307      * @param access
308      *            initial value
309      */
310     public static boolean itemAccess(String permission, Element item, boolean access) {
311         return webpageAccess(permission, getWebpageID(item), access);
312     }
313 
314     /**
315      * Verifies a single webpage on access according to $permission. Falls back to version without query
316      * if no rule for exact query string exists.
317      *
318      * @param permission an ACL permission
319      * @param webpageId webpage to check
320      * @param access
321      *            initial value
322      */
323     public static boolean webpageAccess(String permission, String webpageId, boolean access) {
324         List<String> ruleIDs = getAllWebpageACLIDs(webpageId);
325         return ruleIDs.stream()
326             .filter(objID -> MCRAccessManager.hasRule(objID, permission))
327             .findFirst()
328             .map(objID -> MCRAccessManager.checkPermission(objID, permission))
329             .orElse(access);
330     }
331 
332     private static List<String> getAllWebpageACLIDs(String webpageID) {
333         String webpageACLID = getWebpageACLID(webpageID);
334         List<String> webpageACLIDs = new ArrayList<>(2);
335         webpageACLIDs.add(webpageACLID);
336         int queryIndex = webpageACLID.indexOf('?');
337         if (queryIndex != -1) {
338             String baseWebpageACLID = webpageACLID.substring(0, queryIndex);
339             webpageACLIDs.add(baseWebpageACLID);
340         }
341         return webpageACLIDs;
342     }
343 
344     public static String getWebpageACLID(String webpageID) {
345         return OBJIDPREFIX_WEBPAGE + webpageID;
346     }
347 
348     private static String getWebpageID(Element item) {
349         return item == null ? null : item.getAttributeValue("href", item.getAttributeValue("dir"));
350     }
351 
352     /**
353      * Returns the navigation.xml as org.jdom2.document, using a cache the
354      * enhance loading time.
355      *
356      * @return navigation.xml as org.jdom2.document
357      */
358     public static Document getNavi() {
359         return NAV_DOCUMENT_CACHE.getUnchecked(NAV_RESOURCE).parsedDocument;
360     }
361 
362     /**
363      * Returns the navigation.xml as File.
364      * This file may not exist yet as navigation.xml may be served as a web resource.
365      * Use {@link #getNavigationURL()} to get access to the actual web resource.
366      */
367     public static File getNavigationFile() {
368         String realPath = SERVLET_CONTEXT.getRealPath(NAV_RESOURCE);
369         if (realPath == null) {
370             return null;
371         }
372         return new File(realPath);
373     }
374 
375     /**
376      * Returns the navigation.xml as URL.
377      *
378      * Use this method if you need to parse it on your own.
379      */
380     public static URL getNavigationURL() {
381         try {
382             return SERVLET_CONTEXT.getResource(NAV_RESOURCE);
383         } catch (MalformedURLException e) {
384             throw new MCRException("Error while resolving navigation.xml", e);
385         }
386     }
387 
388     public static org.w3c.dom.Document getPersonalNavigation() throws JDOMException, XPathExpressionException {
389         Document navi = getNavi();
390         DOMOutputter accessCleaner = new DOMOutputter(new AccessCleaningDOMOutputProcessor());
391         org.w3c.dom.Document personalNavi = accessCleaner.output(navi);
392         XPath xpath = javax.xml.xpath.XPathFactory.newInstance().newXPath();
393         NodeList emptyGroups = (NodeList) xpath.evaluate("/navigation/menu/group[not(item)]", personalNavi,
394             XPathConstants.NODESET);
395         for (int i = 0; i < emptyGroups.getLength(); ++i) {
396             org.w3c.dom.Element group = (org.w3c.dom.Element) emptyGroups.item(i);
397             group.getParentNode().removeChild(group);
398         }
399         NodeList emptyMenu = (NodeList) xpath.evaluate("/navigation/menu[not(item or group)]", personalNavi,
400             XPathConstants.NODESET);
401         for (int i = 0; i < emptyMenu.getLength(); ++i) {
402             org.w3c.dom.Element menu = (org.w3c.dom.Element) emptyMenu.item(i);
403             menu.getParentNode().removeChild(menu);
404         }
405         NodeList emptyNodes = (NodeList) xpath.evaluate("//text()[normalize-space(.) = '']", personalNavi,
406             XPathConstants.NODESET);
407         for (int i = 0; i < emptyNodes.getLength(); ++i) {
408             Node emptyTextNode = emptyNodes.item(i);
409             emptyTextNode.getParentNode().removeChild(emptyTextNode);
410         }
411         personalNavi.normalizeDocument();
412         if (LOGGER.isDebugEnabled()) {
413             try {
414                 String encoding = "UTF-8";
415                 TransformerFactory tf = TransformerFactory.newInstance();
416                 Transformer transformer = tf.newTransformer();
417                 transformer.setOutputProperty(OutputKeys.OMIT_XML_DECLARATION, "no");
418                 transformer.setOutputProperty(OutputKeys.METHOD, "xml");
419                 transformer.setOutputProperty(OutputKeys.INDENT, "yes");
420                 transformer.setOutputProperty(OutputKeys.ENCODING, encoding);
421                 transformer.setOutputProperty("{http://xml.apache.org/xslt}indent-amount", "4");
422                 ByteArrayOutputStream bout = new ByteArrayOutputStream();
423                 transformer.transform(new DOMSource(personalNavi), new StreamResult(bout));
424                 LOGGER.debug("personal navigation: {}", bout.toString(encoding));
425             } catch (IllegalArgumentException | TransformerFactoryConfigurationError | TransformerException
426                 | UnsupportedEncodingException e) {
427                 LOGGER.warn("Error while getting debug information.", e);
428             }
429 
430         }
431         return personalNavi;
432     }
433 
434     public static long getDuration(long startTime) {
435         return System.currentTimeMillis() - startTime;
436     }
437 
438     public static String getWebpageObjIDPrefix() {
439         return OBJIDPREFIX_WEBPAGE;
440     }
441 
442     public static boolean hasRule(String permission, String webpageID) {
443         MCRAccessInterface am = MCRAccessManager.getAccessImpl();
444         if (am instanceof MCRRuleAccessInterface) {
445             return ((MCRRuleAccessInterface) am).hasRule(getWebpageACLID(webpageID), permission);
446         } else {
447             return true;
448         }
449     }
450 
451     public static String getRuleID(String permission, String webpageID) {
452         MCRAccessStore as = MCRAccessStore.getInstance();
453         String ruleID = as.getRuleID(getWebpageACLID(webpageID), permission);
454         if (ruleID != null) {
455             return ruleID;
456         } else {
457             return "";
458         }
459     }
460 
461     public static String getRuleDescr(String permission, String webpageID) {
462         MCRAccessInterface am = MCRAccessManager.getAccessImpl();
463         String ruleDes = null;
464         if (am instanceof MCRRuleAccessInterface) {
465             ruleDes = ((MCRRuleAccessInterface) am).getRuleDescription(getWebpageACLID(webpageID), permission);
466         }
467         if (ruleDes != null) {
468             return ruleDes;
469         } else {
470             return "";
471         }
472     }
473 
474     public static String getPermission2ReadWebpage() {
475         return PERMISSION_READ;
476     }
477 
478     private static class DocumentHolder {
479         URL docURL;
480 
481         Document parsedDocument;
482 
483         long lastModified;
484 
485         DocumentHolder(URL url) throws JDOMException, IOException {
486             docURL = url;
487             parseDocument();
488         }
489 
490         public boolean isValid(URL url) throws IOException {
491             return docURL.equals(url) && lastModified == getLastModified();
492         }
493 
494         private void parseDocument() throws JDOMException, IOException {
495             lastModified = getLastModified();
496             LOGGER.info("Parsing: {}", docURL);
497             parsedDocument = new SAXBuilder(XMLReaders.NONVALIDATING).build(docURL);
498         }
499 
500         private long getLastModified() throws IOException {
501             URLConnection urlConnection = docURL.openConnection();
502             return urlConnection.getLastModified();
503         }
504     }
505 
506     private static class AccessCleaningDOMOutputProcessor extends AbstractDOMOutputProcessor {
507 
508         @Override
509         protected org.w3c.dom.Element printElement(FormatStack fstack, NamespaceStack nstack,
510             org.w3c.dom.Document basedoc, Element element) {
511             Attribute href = element.getAttribute("href");
512             return (href == null || itemAccess(PERMISSION_READ, element, true)) ? super.printElement(fstack, nstack,
513                 basedoc, element) : null;
514         }
515 
516     }
517 }