private GraphObject extractActiveElement( final DOMNode node, final Set<String> dataKeys, final String parentId, final ActiveElementState state, final int depth) { final GraphObjectMap activeElement = new GraphObjectMap(); activeElement.put(GraphObject.id, node.getUuid()); activeElement.put(GraphObject.type, node.getType()); activeElement.put(DOMElement.dataKey, StringUtils.join(dataKeys, ",")); activeElement.put(Content.content, node.getProperty(Content.content)); switch (state) { case Button: activeElement.put(actionProperty, node.getProperty(DOMElement._action)); break; case Link: activeElement.put(actionProperty, node.getProperty(Link._href)); break; case Query: extractQueries(activeElement, node); break; } activeElement.put(stateProperty, state.name()); activeElement.put(recursionDepthProperty, depth); activeElement.put(parentIdProperty, parentId); return activeElement; }
@Override public void render(RenderContext renderContext, int depth) throws FrameworkException { renderContext.setPage(this); // Skip DOCTYPE node DOMNode subNode = (DOMNode) this.getFirstChild().getNextSibling(); if (subNode == null) { subNode = (DOMNode) super.getFirstChild(); } else { renderContext.getBuffer().append("<!DOCTYPE html>\n"); } while (subNode != null) { if (subNode.isNotDeleted() && securityContext.isVisible(subNode)) { subNode.render(renderContext, depth); } subNode = (DOMNode) subNode.getNextSibling(); } }
// ----- private methods ----- private void collectActiveElements( final List<GraphObject> resultList, final DOMNode root, final Set<String> parentDataKeys, final String parent, final int depth) { final String childDataKey = root.getProperty(DOMElement.dataKey); final Set<String> dataKeys = new LinkedHashSet<>(parentDataKeys); String parentId = parent; int dataCentricDepth = depth; if (!StringUtils.isEmpty(childDataKey)) { dataKeys.add(childDataKey); dataCentricDepth++; } final ActiveElementState state = isActive(root, dataKeys); if (!state.equals(ActiveElementState.None)) { resultList.add(extractActiveElement(root, dataKeys, parentId, state, depth)); if (state.equals(ActiveElementState.Query)) { parentId = root.getUuid(); } } for (final DOMChildren children : root.getChildRelationships()) { final DOMNode child = children.getTargetNode(); collectActiveElements(resultList, child, dataKeys, parentId, dataCentricDepth); } }
private boolean extractQuery( final GraphObjectMap activeElement, final DOMNode node, final Property<String> queryKey) { if (!StringUtils.isEmpty(node.getProperty(queryKey))) { activeElement.put(queryProperty, node.getProperty(queryKey)); return true; } return false; }
@Override protected void handleNewChild(Node newChild) { for (final DOMNode child : getAllChildNodes()) { try { child.setProperty(ownerDocument, this); } catch (FrameworkException ex) { logger.log(Level.WARNING, "", ex); } } }
private Node importNode( final Node node, final boolean deep, final boolean removeParentFromSourceNode) throws DOMException { if (node instanceof DOMNode) { final DOMNode domNode = (DOMNode) node; // step 1: use type-specific import impl. Node importedNode = domNode.doImport(Page.this); // step 2: do recursive import? if (deep && domNode.hasChildNodes()) { // FIXME: is it really a good idea to do the // recursion inside of a transaction? Node child = domNode.getFirstChild(); while (child != null) { // do not remove parent for child nodes importNode(child, deep, false); child = child.getNextSibling(); logger.log(Level.INFO, "sibling is {0}", child); } } // step 3: remove node from its current parent // (Note that this step needs to be done last in // (order for the child to be able to find its // siblings.) if (removeParentFromSourceNode) { // only do this for the actual source node, do not remove // child nodes from its parents Node _parent = domNode.getParentNode(); if (_parent != null) { _parent.removeChild(domNode); } } return importedNode; } return null; }
private void test( final DOMNode p, final DOMNode text, final String content, final String expected, final RenderContext context) throws FrameworkException { text.setTextContent(content); // clear queue context.getBuffer().getQueue().clear(); p.render(context, 0); assertEquals( "Invalid JavaScript evaluation result", expected, concat(context.getBuffer().getQueue())); }
@Override public String toString() { if (existingNode instanceof Content) { return "Update Content(" + existingNode.getIdHashOrProperty() + ") with " + newNode.getIdHashOrProperty(); } else { return "Update " + newNode.getProperty(DOMElement.tag) + "(" + existingNode.getIdHashOrProperty() + ") with " + newNode.getIdHashOrProperty(); } }
private Node adoptNode(final Node node, final boolean removeParentFromSourceNode) throws DOMException { if (node instanceof DOMNode) { final DOMNode domNode = (DOMNode) node; // step 2: do recursive import? if (domNode.hasChildNodes()) { Node child = domNode.getFirstChild(); while (child != null) { // do not remove parent for child nodes adoptNode(child, false); child = child.getNextSibling(); } } // step 3: remove node from its current parent // (Note that this step needs to be done last in // (order for the child to be able to find its // siblings.) if (removeParentFromSourceNode) { // only do this for the actual source node, do not remove // child nodes from its parents Node _parent = domNode.getParentNode(); if (_parent != null) { _parent.removeChild(domNode); } } // step 1: use type-specific adopt impl. Node adoptedNode = domNode.doAdopt(Page.this); return adoptedNode; } return null; }
@Override protected void checkHierarchy(Node otherNode) throws DOMException { // verify that this document has only one document element if (getDocumentElement() != null) { throw new DOMException( DOMException.HIERARCHY_REQUEST_ERR, HIERARCHY_REQUEST_ERR_MESSAGE_DOCUMENT); } if (!(otherNode instanceof Html || otherNode instanceof Comment || otherNode instanceof Template)) { throw new DOMException( DOMException.HIERARCHY_REQUEST_ERR, HIERARCHY_REQUEST_ERR_MESSAGE_ELEMENT); } super.checkHierarchy(otherNode); }
@Override protected void doGet(final HttpServletRequest request, final HttpServletResponse response) { final Authenticator auth = config.getAuthenticator(); final SecurityContext securityContext; final App app; try { String path = request.getPathInfo(); // check for registration (has its own tx because of write access if (checkRegistration(auth, request, response, path)) { return; } // isolate request authentication in a transaction try (final Tx tx = StructrApp.getInstance().tx()) { securityContext = auth.initializeAndExamineRequest(request, response); tx.success(); } app = StructrApp.getInstance(securityContext); try (final Tx tx = app.tx()) { // Ensure access mode is frontend securityContext.setAccessMode(AccessMode.Frontend); request.setCharacterEncoding("UTF-8"); // Important: Set character encoding before calling response.getWriter() !!, see Servlet // Spec 5.4 response.setCharacterEncoding("UTF-8"); boolean dontCache = false; logger.log(Level.FINE, "Path info {0}", path); // don't continue on redirects if (response.getStatus() == 302) { return; } Principal user = securityContext.getUser(false); if (user != null) { // Don't cache if a user is logged in dontCache = true; } final RenderContext renderContext = RenderContext.getInstance(request, response, getEffectiveLocale(request)); renderContext.setResourceProvider(config.getResourceProvider()); EditMode edit = renderContext.getEditMode(user); DOMNode rootElement = null; AbstractNode dataNode = null; String[] uriParts = PathHelper.getParts(path); if ((uriParts == null) || (uriParts.length == 0)) { // find a visible page rootElement = findIndexPage(securityContext); logger.log(Level.FINE, "No path supplied, trying to find index page"); } else { if (rootElement == null) { rootElement = findPage(securityContext, request, path); } else { dontCache = true; } } if (rootElement == null) { // No page found // Look for a file File file = findFile(securityContext, request, path); if (file != null) { streamFile(securityContext, file, request, response, edit); return; } // store remaining path parts in request Matcher matcher = threadLocalUUIDMatcher.get(); boolean requestUriContainsUuids = false; for (int i = 0; i < uriParts.length; i++) { request.setAttribute(uriParts[i], i); matcher.reset(uriParts[i]); // set to "true" if part matches UUID pattern requestUriContainsUuids |= matcher.matches(); } if (!requestUriContainsUuids) { // Try to find a data node by name dataNode = findFirstNodeByName(securityContext, request, path); } else { dataNode = findNodeByUuid(securityContext, PathHelper.getName(path)); } if (dataNode != null) { // Last path part matches a data node // Remove last path part and try again searching for a page // clear possible entry points request.removeAttribute(POSSIBLE_ENTRY_POINTS); rootElement = findPage( securityContext, request, StringUtils.substringBeforeLast(path, PathHelper.PATH_SEP)); renderContext.setDetailsDataObject(dataNode); // Start rendering on data node if (rootElement == null && dataNode instanceof DOMNode) { rootElement = ((DOMNode) dataNode); } } } // Still nothing found, do error handling if (rootElement == null) { // Check if security context has set an 401 status if (response.getStatus() == HttpServletResponse.SC_UNAUTHORIZED) { try { UiAuthenticator.writeUnauthorized(response); } catch (IllegalStateException ise) { } } else { rootElement = notFound(response, securityContext); } } if (rootElement == null) { return; } if (EditMode.WIDGET.equals(edit) || dontCache) { setNoCacheHeaders(response); } if (!securityContext.isVisible(rootElement)) { rootElement = notFound(response, securityContext); if (rootElement == null) { return; } } if (securityContext.isVisible(rootElement)) { if (!EditMode.WIDGET.equals(edit) && !dontCache && notModifiedSince(request, response, rootElement, dontCache)) { ServletOutputStream out = response.getOutputStream(); out.flush(); // response.flushBuffer(); out.close(); } else { // prepare response response.setCharacterEncoding("UTF-8"); String contentType = rootElement.getProperty(Page.contentType); if (contentType != null && contentType.equals("text/html")) { contentType = contentType.concat(";charset=UTF-8"); response.setContentType(contentType); } else { // Default response.setContentType("text/html;charset=UTF-8"); } response.setHeader("Strict-Transport-Security", "max-age=60"); response.setHeader("X-Content-Type-Options", "nosniff"); response.setHeader("X-Frame-Options", "SAMEORIGIN"); response.setHeader("X-XSS-Protection", "1; mode=block"); // async or not? boolean isAsync = HttpService.parseBoolean( Services.getBaseConfiguration().getProperty(HttpService.ASYNC), true); if (isAsync) { final AsyncContext async = request.startAsync(); final ServletOutputStream out = async.getResponse().getOutputStream(); final AtomicBoolean finished = new AtomicBoolean(false); final DOMNode rootNode = rootElement; threadPool.submit( new Runnable() { @Override public void run() { try (final Tx tx = app.tx()) { // final long start = System.currentTimeMillis(); // render rootNode.render(securityContext, renderContext, 0); finished.set(true); // final long end = System.currentTimeMillis(); // System.out.println("Done in " + (end-start) + " ms"); tx.success(); } catch (Throwable t) { t.printStackTrace(); final String errorMsg = t.getMessage(); try { // response.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR); response.sendError( HttpServletResponse.SC_INTERNAL_SERVER_ERROR, errorMsg); finished.set(true); } catch (IOException ex) { ex.printStackTrace(); } } } }); // start output write listener out.setWriteListener( new WriteListener() { @Override public void onWritePossible() throws IOException { try { final Queue<String> queue = renderContext.getBuffer().getQueue(); while (out.isReady()) { String buffer = null; synchronized (queue) { buffer = queue.poll(); } if (buffer != null) { out.print(buffer); } else { if (finished.get()) { async.complete(); response.setStatus(HttpServletResponse.SC_OK); // prevent this block from being called again break; } Thread.sleep(1); } } } catch (Throwable t) { t.printStackTrace(); } } @Override public void onError(Throwable t) { t.printStackTrace(); } }); } else { final StringRenderBuffer buffer = new StringRenderBuffer(); renderContext.setBuffer(buffer); // render rootElement.render(securityContext, renderContext, 0); response.getOutputStream().write(buffer.getBuffer().toString().getBytes("utf-8")); response.getOutputStream().flush(); response.getOutputStream().close(); } } } else { notFound(response, securityContext); } tx.success(); } catch (FrameworkException fex) { fex.printStackTrace(); logger.log(Level.SEVERE, "Exception while processing request", fex); } } catch (IOException | FrameworkException t) { t.printStackTrace(); logger.log(Level.SEVERE, "Exception while processing request", t); UiAuthenticator.writeInternalServerError(response); } }
@Override public void processMessage(final WebSocketMessage webSocketData) { final SecurityContext securityContext = getWebSocket().getSecurityContext(); final String id = webSocketData.getId(); if (id != null) { final NodeInterface node = getNode(id); if (node != null) { if (node instanceof DOMNode) { // Use new DOM interface DOMNode domNode = (DOMNode) node; try { domNode.getParentNode().removeChild(domNode); // Remove node from page final PropertyMap changedProperties = new PropertyMap(); changedProperties.put(DOMNode.syncedNodes, Collections.EMPTY_LIST); changedProperties.put(DOMNode.pageId, null); domNode.setProperties(securityContext, changedProperties); } catch (DOMException | FrameworkException ex) { logger.error("Could not remove node from page " + domNode, ex); getWebSocket() .send(MessageBuilder.status().code(422).message(ex.getMessage()).build(), true); } } else { final App app = StructrApp.getInstance(securityContext); try { // Old style: Delete all incoming CONTAINS rels for (AbstractRelationship rel : node.getIncomingRelationships()) { if ("CONTAINS".equals(rel.getType())) { app.delete(rel); } } } catch (Throwable t) { logger.error("Could not delete relationship", t); getWebSocket() .send( MessageBuilder.status() .code(400) .message("Error in RemoveCommand: " + t.getMessage()) .build(), true); } } } else { getWebSocket().send(MessageBuilder.status().code(404).build(), true); } } else { getWebSocket() .send( MessageBuilder.status() .code(400) .message("RemoveCommand called with empty id") .build(), true); } }
@Override public void processMessage(WebSocketMessage webSocketData) { final Map<String, Object> nodeData = webSocketData.getNodeData(); final String parentId = (String) nodeData.get("parentId"); final String pageId = webSocketData.getPageId(); nodeData.remove("parentId"); if (pageId != null) { // check for parent ID before creating any nodes if (parentId == null) { getWebSocket() .send( MessageBuilder.status() .code(422) .message("Cannot add node without parentId") .build(), true); return; } // check if content node with given ID exists final DOMNode contentNode = getDOMNode(parentId); if (contentNode == null) { getWebSocket() .send(MessageBuilder.status().code(404).message("Parent node not found").build(), true); return; } final Document document = getPage(pageId); if (document != null) { final String tagName = (String) nodeData.get("tagName"); nodeData.remove("tagName"); final DOMNode parentNode = (DOMNode) contentNode.getParentNode(); try { DOMNode elementNode = null; if (tagName != null && !tagName.isEmpty()) { elementNode = (DOMNode) document.createElement(tagName); } // append new node to parent parent node if (elementNode != null) { parentNode.appendChild(elementNode); } // append new node to parent parent node if (elementNode != null) { // append content node to new node elementNode.appendChild(contentNode); } } catch (DOMException dex) { // send DOM exception getWebSocket() .send(MessageBuilder.status().code(422).message(dex.getMessage()).build(), true); } } else { getWebSocket() .send(MessageBuilder.status().code(404).message("Page not found").build(), true); } } else { getWebSocket() .send( MessageBuilder.status() .code(422) .message("Cannot create node without pageId") .build(), true); } }
@Override public void processMessage(final WebSocketMessage webSocketData) { String id = webSocketData.getId(); Map<String, Object> nodeData = webSocketData.getNodeData(); String parentId = (String) nodeData.get("parentId"); // check node to append if (id == null) { getWebSocket() .send( MessageBuilder.status() .code(422) .message("Cannot append node, no id is given") .build(), true); return; } // check for parent ID if (parentId == null) { getWebSocket() .send( MessageBuilder.status().code(422).message("Cannot add node without parentId").build(), true); return; } // check if parent node with given ID exists AbstractNode parentNode = getNode(parentId); if (parentNode == null) { getWebSocket() .send(MessageBuilder.status().code(404).message("Parent node not found").build(), true); return; } if (parentNode instanceof DOMNode) { DOMNode parentDOMNode = getDOMNode(parentId); if (parentDOMNode == null) { getWebSocket() .send( MessageBuilder.status().code(422).message("Parent node is no DOM node").build(), true); return; } DOMNode node = (DOMNode) getDOMNode(id); // append node to parent if (node != null) { parentDOMNode.appendChild(node); } } else { // send exception getWebSocket() .send( MessageBuilder.status() .code(422) .message("Cannot use given node, not instance of DOMNode") .build(), true); } }
private ActiveElementState isActive(final DOMNode node, final Set<String> dataKeys) { if (!StringUtils.isEmpty(node.getProperty(DOMElement.dataKey))) { return ActiveElementState.Query; } if (!StringUtils.isEmpty(node.getProperty(DOMElement.restQuery))) { return ActiveElementState.Query; } if (!StringUtils.isEmpty(node.getProperty(DOMElement.cypherQuery))) { return ActiveElementState.Query; } if (!StringUtils.isEmpty(node.getProperty(DOMElement.xpathQuery))) { return ActiveElementState.Query; } if (!StringUtils.isEmpty(node.getProperty(DOMElement.functionQuery))) { return ActiveElementState.Query; } /* attributes to check for !isEmpty: - data-structr-action - data-structr-attributes - data-structr-raw-value */ if (node.getProperty(DOMElement._action) != null || node.getProperty(DOMElement._attributes) != null || node.getProperty(DOMElement._rawValue) != null) { return ActiveElementState.Button; } if (node.getProperty(Content.content) != null && !dataKeys.isEmpty()) { if (containsDataKeyReference(node.getProperty(Content.content), dataKeys)) { return ActiveElementState.Content; } } if (node.getProperty(DOMElement._id) != null && !dataKeys.isEmpty()) { if (containsDataKeyReference(node.getProperty(DOMElement._id), dataKeys)) { return ActiveElementState.Content; } } if (node.getProperty(Link._href) != null && !dataKeys.isEmpty()) { if (containsDataKeyReference(node.getProperty(Link._href), dataKeys)) { return ActiveElementState.Link; } } if (node.getProperty(Input._value) != null && !dataKeys.isEmpty()) { if (containsDataKeyReference(node.getProperty(Input._value), dataKeys)) { return ActiveElementState.Input; } } // last option: just some hidden element.. if (!StringUtils.isEmpty(node.getProperty(DOMNode.hideConditions))) { return ActiveElementState.Content; } if (!StringUtils.isEmpty(node.getProperty(DOMNode.showConditions))) { return ActiveElementState.Content; } if (node.getProperty(DOMNode.hideOnIndex)) { return ActiveElementState.Content; } if (node.getProperty(DOMNode.hideOnDetail)) { return ActiveElementState.Content; } return ActiveElementState.None; }
// ----- interface InvertibleModificationOperation ----- @Override public void apply(final App app, final Page sourcePage, final Page newPage) throws FrameworkException { existingNode.updateFromNode(newNode); }
@Test public void testScripting() { NodeInterface detailsDataObject = null; Page page = null; DOMNode html = null; DOMNode head = null; DOMNode body = null; DOMNode title = null; DOMNode div = null; DOMNode p = null; DOMNode text = null; try (final Tx tx = app.tx()) { detailsDataObject = app.create(TestOne.class, "TestOne"); page = Page.createNewPage(securityContext, "testpage"); page.setProperties( page.getSecurityContext(), new PropertyMap(Page.visibleToPublicUsers, true)); assertTrue(page != null); assertTrue(page instanceof Page); html = (DOMNode) page.createElement("html"); head = (DOMNode) page.createElement("head"); body = (DOMNode) page.createElement("body"); title = (DOMNode) page.createElement("title"); div = (DOMNode) page.createElement("div"); p = (DOMNode) page.createElement("p"); text = (DOMNode) page.createTextNode("x"); // add HTML element to page page.appendChild(html); // add HEAD and BODY elements to HTML html.appendChild(head); html.appendChild(body); // add TITLE element to HEAD head.appendChild(title); body.appendChild(div); div.appendChild(p); final PropertyMap changedProperties = new PropertyMap(); changedProperties.put(DOMElement.restQuery, "/divs"); changedProperties.put(DOMElement.dataKey, "div"); p.setProperties(p.getSecurityContext(), changedProperties); p.appendChild(text); tx.success(); } catch (FrameworkException fex) { fail("Unexpected exception"); } try (final Tx tx = app.tx()) { final RenderContext ctx = new RenderContext( securityContext, new RequestMockUp(), new ResponseMockUp(), RenderContext.EditMode.NONE); ctx.setDetailsDataObject(detailsDataObject); ctx.setPage(page); test(p, text, "${{ return Structr.get('div').id}}", "<p>" + div.getUuid() + "</p>", ctx); test(p, text, "${{ return Structr.get('page').id}}", "<p>" + page.getUuid() + "</p>", ctx); test(p, text, "${{ return Structr.get('parent').id}}", "<p>" + p.getUuid() + "</p>", ctx); tx.success(); } catch (FrameworkException fex) { logger.warn("", fex); fail("Unexpected exception."); } }
@Override public void processMessage(WebSocketMessage webSocketData) { final Map<String, Object> nodeData = webSocketData.getNodeData(); final String parentId = (String) nodeData.get("parentId"); final String childContent = (String) nodeData.get("childContent"); final String pageId = webSocketData.getPageId(); nodeData.remove("parentId"); if (pageId != null) { // check for parent ID before creating any nodes if (parentId == null) { getWebSocket() .send( MessageBuilder.status() .code(422) .message("Cannot add node without parentId") .build(), true); return; } // check if parent node with given ID exists final DOMNode parentNode = getDOMNode(parentId); if (parentNode == null) { getWebSocket() .send(MessageBuilder.status().code(404).message("Parent node not found").build(), true); return; } final Document document = getPage(pageId); if (document != null) { final String tagName = (String) nodeData.get("tagName"); final App app = StructrApp.getInstance(); nodeData.remove("tagName"); try { app.beginTx(); DOMNode newNode; if (tagName != null && !tagName.isEmpty()) { newNode = (DOMNode) document.createElement(tagName); } else { newNode = (DOMNode) document.createTextNode("#text"); } // append new node to parent if (newNode != null) { parentNode.appendChild(newNode); for (Entry entry : nodeData.entrySet()) { String key = (String) entry.getKey(); Object val = entry.getValue(); PropertyKey propertyKey = StructrApp.getConfiguration() .getPropertyKeyForDatabaseName(newNode.getClass(), key); if (propertyKey != null) { try { Object convertedValue = val; PropertyConverter inputConverter = propertyKey.inputConverter(SecurityContext.getSuperUserInstance()); if (inputConverter != null) { convertedValue = inputConverter.convert(val); } // newNode.unlockReadOnlyPropertiesOnce(); newNode.setProperty(propertyKey, convertedValue); } catch (FrameworkException fex) { logger.log( Level.WARNING, "Unable to set node property {0} of node {1} to {2}: {3}", new Object[] {propertyKey, newNode.getUuid(), val, fex.getMessage()}); } } } // create a child text node if content is given if (StringUtils.isNotBlank(childContent)) { DOMNode childNode = (DOMNode) document.createTextNode(childContent); if (newNode != null) { newNode.appendChild(childNode); } } } app.commitTx(); } catch (DOMException dex) { // send DOM exception getWebSocket() .send(MessageBuilder.status().code(422).message(dex.getMessage()).build(), true); } catch (FrameworkException ex) { Logger.getLogger(CreateAndAppendDOMNodeCommand.class.getName()) .log(Level.SEVERE, null, ex); } finally { app.finishTx(); } } else { getWebSocket() .send(MessageBuilder.status().code(404).message("Page not found").build(), true); } } else { getWebSocket() .send( MessageBuilder.status() .code(422) .message("Cannot create node without pageId") .build(), true); } }