1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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
86
87
88 public class MCRLayoutUtilities {
89
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
145
146
147
148
149
150
151
152
153
154
155
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
171
172
173
174
175
176
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
192
193
194
195
196
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
225
226
227
228
229
230
231
232
233
234
235
236 public static boolean getAccess(String webpageID, String permission, int strategy) {
237 Element item = getItem(webpageID);
238
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
256
257
258
259
260
261
262
263
264
265
266
267
268
269 public static boolean getAccess(String webpageID, String permission, int strategy, String blockerWebpageID) {
270 Element item = getItem(webpageID);
271
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
287
288
289
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
303
304
305
306
307
308
309
310 public static boolean itemAccess(String permission, Element item, boolean access) {
311 return webpageAccess(permission, getWebpageID(item), access);
312 }
313
314
315
316
317
318
319
320
321
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
354
355
356
357
358 public static Document getNavi() {
359 return NAV_DOCUMENT_CACHE.getUnchecked(NAV_RESOURCE).parsedDocument;
360 }
361
362
363
364
365
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
377
378
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 }