diff src/nabble/view/lib/Jtp.java @ 0:7ecd1a4ef557

add content
author Franklin Schmidt <fschmidt@gmail.com>
date Thu, 21 Mar 2019 19:15:52 -0600
parents
children 18cf4872fd7f
line wrap: on
line diff
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/nabble/view/lib/Jtp.java	Thu Mar 21 19:15:52 2019 -0600
@@ -0,0 +1,966 @@
+package nabble.view.lib;
+
+import fschmidt.util.java.HtmlUtils;
+import fschmidt.util.servlet.ServletUtils;
+import nabble.model.DailyNumber;
+import nabble.model.Init;
+import nabble.model.ModelHome;
+import nabble.model.Node;
+import nabble.model.NodeIterator;
+import nabble.model.NodeSearcher;
+import nabble.model.Person;
+import nabble.model.Site;
+import nabble.model.User;
+import nabble.naml.compiler.Template;
+import nabble.naml.compiler.TemplatePrintWriter;
+import nabble.naml.namespaces.BasicNamespace;
+import nabble.view.web.forum.Permalink;
+import nabble.view.web.template.NabbleNamespace;
+import nabble.view.web.template.NodeNamespace;
+import nabble.view.web.template.UserNamespace;
+import org.apache.commons.fileupload.DiskFileUpload;
+import org.apache.commons.fileupload.FileItem;
+import org.apache.commons.fileupload.FileUploadException;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import javax.servlet.ServletException;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import java.io.IOException;
+import java.io.StringWriter;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.Date;
+import java.util.HashMap;
+import java.util.LinkedHashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.StringTokenizer;
+import java.util.TreeSet;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+
+public final class Jtp {
+
+	private static final Logger logger = LoggerFactory.getLogger(Jtp.class);
+
+	private static final String ANONYMOUS_COOKIE_ID = "anonymousId";
+	private static final String ANONYMOUS_COOKIE_NAME = "anonymousName";
+
+	private Jtp() {}  // never
+
+	public static Person getVisitor(HttpServletRequest request, HttpServletResponse response)
+		throws ServletException
+	{
+		Person visitor = getUser(request,response);
+		if( visitor != null )
+			return visitor;
+		return getOrCreateAnonymous(request,response);
+	}
+
+	private static Person getAnonymous(HttpServletRequest request) {
+		String anonymousId = ServletUtils.getCookieValue(request, ANONYMOUS_COOKIE_ID);
+		if (anonymousId == null)
+			return null;
+		String anonymousName = ServletUtils.getCookieValue(request, ANONYMOUS_COOKIE_NAME);
+		return getSite(request).getAnonymous(anonymousId, anonymousName == null? null : HtmlUtils.urlDecode(anonymousName));
+	}
+
+	private static Person getOrCreateAnonymous(HttpServletRequest request, HttpServletResponse response)
+		throws ServletException
+	{
+		Person anonymous = getAnonymous(request);
+		if (anonymous == null) {
+			Site site = getSiteNotNull(request);
+			String cookie = site.newAnonymousCookie();
+			anonymous = site.getAnonymous(cookie,null);
+			ServletUtils.setCookie(request, response, ANONYMOUS_COOKIE_ID, cookie, true, null);
+		}
+		return anonymous;
+	}
+
+	public static User getUser(HttpServletRequest request, HttpServletResponse response)
+		throws ServletException
+	{
+		return getUser(request);
+	}
+
+	public static User getUser(HttpServletRequest request)
+		throws ServletException
+	{
+		User user = null;
+		String userId = ServletUtils.getCookieValue(request,"userId");
+		if (userId != null && userId.length() > 0) {
+			user = getSiteNotNull(request).getUser(Long.valueOf(userId));
+		}
+		if( user==null )
+			return null;
+		if( ServletUtils.getCookieValue(request,"username")==null)
+			return null;
+		String pwd = ServletUtils.getCookieValue(request,"password");
+		if ( pwd==null )
+			return null;
+		String passcookie = HtmlUtils.urlDecode(pwd);
+		if( ! (user.isRegistered() && user.checkPasscookie(passcookie)) )
+			return null;
+		trackUser(request, user);
+		return user;
+	}
+
+	public static void doLogin(HttpServletRequest request, HttpServletResponse response, User user, boolean save)
+		throws IOException
+	{
+		if( !user.isRegistered() )
+			throw new RuntimeException("user must be registered to login");
+
+		ServletUtils.setCookie(request,response,"userId", String.valueOf(user.getId()), save, null);
+		ServletUtils.setCookie(request,response,"password", HtmlUtils.urlEncode(user.getPasscookie()), save, null);
+		ServletUtils.setCookie(request,response,"username", HtmlUtils.urlEncode(user.getName()), save, null);
+		dontCache(response);
+
+		DailyNumber.logins.inc();
+		trackUser(request, user);
+
+		// fix anonymous nodes from this user
+		String anonymousId = ServletUtils.getCookieValue(request, ANONYMOUS_COOKIE_ID);
+		if (anonymousId != null) {
+			user.moveToRegisteredAccount(anonymousId);
+			ServletUtils.removeCookie(request, response, ANONYMOUS_COOKIE_ID, null);
+			ServletUtils.removeCookie(request, response, ANONYMOUS_COOKIE_NAME, null);
+		}
+	}
+
+	private static Set trackUsers = Init.get("trackUsers", Collections.EMPTY_SET);
+
+	private static void trackUser(HttpServletRequest request, User user) {
+		long siteId = user.getSite().getId();
+		String siteAndUser = siteId + "|" + user.getId();
+		if (trackUsers.contains(siteAndUser))
+			logger.error("Track User [site=" + siteId + " | user = " + user.getId() + "] = " + getClientIpAddr(request));
+	}
+
+	public static String getClientIpAddr(HttpServletRequest request) {
+		return JtpContextServlet.getClientIpAddr(request);
+	}
+
+	public static void logout(HttpServletRequest request,HttpServletResponse response) {
+		request.getSession().removeAttribute("nextUrl");
+		ServletUtils.removeCookie(request,response,"userId", null);
+		ServletUtils.removeCookie(request,response,"password", null);
+		ServletUtils.removeCookie(request,response,"username", null);
+	}
+
+	public static void login(String msg, HttpServletRequest request, HttpServletResponse response)
+		throws IOException, ServletException
+	{
+		String nextUrl = getCurrentPath(request);
+//if(nextUrl.endsWith("NamlServlet.jtp")) logger.error("nextUrl = "+nextUrl);
+		response.sendRedirect(loginPath(getSiteNotNull(request),msg,nextUrl));
+	}
+
+	public static String getCurrentPath(HttpServletRequest request) {
+		String s = request.getServletPath();
+		String q = request.getQueryString();
+		if( q != null )
+			s += "?" + q;
+		return s;
+	}
+
+	public static String loginPath(Site site,String message,String nextUrl) {
+		Map<String,Object> args = new HashMap<String,Object>();
+		if( message != null )
+			args.put("message",message);
+		if( nextUrl != null )
+			args.put("nextUrl",nextUrl);
+		Template template = site.getTemplate( "login_path",
+			BasicNamespace.class, NabbleNamespace.class
+		);
+		StringWriter sw = new StringWriter();
+		template.run( new TemplatePrintWriter(sw), args,
+			new BasicNamespace(template), new NabbleNamespace(site)
+		);
+		return sw.toString();
+	}
+
+
+	public static String hideNull(Object obj) {
+		return obj==null ? "" : obj.toString();
+	}
+
+	public static String userUrl(Person user) {
+		String base = user.getSite().getBaseUrl();
+		return base + "/template/NamlServlet.jtp?macro=user_nodes&user=" + user.getIdString();
+	}
+
+	public static String userLink(Person user) {
+		return "<a href=\"" + userUrl(user) + "\" rel=\"nofollow\" target=\"_top\">"
+				+user.getNameHtml()+"</a>";
+	}
+
+	public static String userLinkJs(HttpServletRequest request,User user) {
+		StringBuilder buf = new StringBuilder();
+		buf.append("<script>document.write('<a href=\\\"");
+		buf.append(request.getContextPath());
+		buf.append("' +'/user/UserNodes' + '.jtp?'+'user=");
+		buf.append(user.getId());
+		buf.append("\\\" rel=\\\"nofollow\\\" ");
+		buf.append("' + Nabble.embeddedTarget('_top') + '");
+		buf.append(">');</script>");
+		buf.append(user.getNameHtml());
+		buf.append("<script>document.write('</a>');</script>");
+		return buf.toString();
+	}
+
+
+	public static String subjectEncode(String s) {
+		return ViewUtils.subjectEncode(s);
+	}
+
+	public static String link(Node node) {
+		if (node == null)
+			return "";
+		return "<a href=\"" + url(node) + "\">" + node.getSubjectHtml() + "</a>";
+	}
+
+	public static String url(Node node) {
+		return node.getSite().getBaseUrl() + path(node);
+	}
+
+	public static String path(Node node) {
+		switch( node.getKind() ) {
+		case POST:
+			return Permalink.path(null,node);
+		case APP:
+			Site site = node.getSite();
+			Template template = site.getTemplate( "app_path",
+				BasicNamespace.class, NabbleNamespace.class, NodeNamespace.class
+			);
+			StringWriter sw = new StringWriter();
+			template.run( new TemplatePrintWriter(sw), Collections.<String,Object>emptyMap(),
+				new BasicNamespace(template), new NabbleNamespace(site), new NodeNamespace(node)
+			);
+			return sw.toString();
+		}
+		throw new RuntimeException("never");
+	}
+
+	public static String topicViewPath(Node node, long selectedId, TopicView topicView) {
+		Site site = node.getSite();
+		Template template = site.getTemplate( "topic_path",
+			BasicNamespace.class, NabbleNamespace.class, NodeNamespace.class
+		);
+		StringWriter sw = new StringWriter();
+		Map<String,Object> args = new HashMap<String,Object>();
+		args.put( "view", topicView == TopicView.CLASSIC? "classic" : topicView == TopicView.THREADED? "threaded" : "list");
+		args.put( "selected_id", selectedId);
+		template.run( new TemplatePrintWriter(sw), args,
+			new BasicNamespace(template), new NabbleNamespace(site), new NodeNamespace(node)
+		);
+		return sw.toString();
+	}
+
+	public static String link(User user) {
+		return "<a href=\"" + url(user) + "\">" + user.getNameHtml() + "</a>";
+	}
+
+	public static String url(User user) {
+		return user.getSite().getBaseUrl() + path(user);
+	}
+
+	public static String path(User user) {
+		Site site = user.getSite();
+		Template template = site.getTemplate( "path",
+			BasicNamespace.class, NabbleNamespace.class, UserNamespace.class
+		);
+		StringWriter sw = new StringWriter();
+		template.run( new TemplatePrintWriter(sw), Collections.<String,Object>emptyMap(),
+			new BasicNamespace(template), new NabbleNamespace(site), new UserNamespace(user)
+		);
+		return sw.toString();
+	}
+
+	public static void javascriptRedirect(HttpServletRequest request,HttpServletResponse response,Node node)
+		throws IOException, ServletException
+	{
+		Shared.javascriptRedirect(request,response,url(node));
+	}
+
+	public static String getStartFragment(String text,int minSize,int maxSize) {
+		if( text.length() <= maxSize )
+			return text;
+		int i = maxSize;
+		while( !Character.isWhitespace(text.charAt(i)) ) {
+			i--;
+			if( i <= minSize )
+				return text.substring(0,maxSize);
+		}
+		while( Character.isWhitespace(text.charAt(i)) ) {
+			if( i < minSize )
+				return text.substring(0,maxSize);
+			i--;
+		}
+		return text.substring(0,i+1);
+	}
+
+	public static String truncate(String text,int len,String dotdotdot) {
+		return text.length() <= len ? text : text.substring(0,len) + dotdotdot;
+	}
+
+	public static String breakUp(final String text) {
+		return HtmlUtils.breakUp(text,30,true);
+	}
+
+	private static final String PUNCTUATION = "[\\!\"\\&\\'\\(\\)\\*\\+\\,\\.\\/\\:\\;\\<\\=\\>\\?\\@\\[\\]\\^\\_\\`\\{\\|\\}\\~]";
+
+	private static void metaKeywords(final String text,Set<String> words) {
+		StringTokenizer str = new StringTokenizer(text.toLowerCase().replaceAll(PUNCTUATION," "));
+		while( str.hasMoreTokens() ) {
+			String s = str.nextToken();
+			if( "re".equals(s) )
+				continue;
+			words.add(s);
+		}
+	}
+
+	private static final Set<String> invalidKeywords = new TreeSet<String>();
+	static {
+		String[] words = {
+			"hi", "a", "an", "in", "i", "hello", "please",
+			"to", "on", "by", "t", "m", "but", "the", "of",
+			"it", "so", "at", "am", "dear", "me", "and", "are",
+			"wonder", "from", "be", "been", "is",
+			"was", "if", "this", "that", "there"
+		};
+		for (String word : words) {
+			invalidKeywords.add(word);
+		}
+	}
+
+	private static String metaKeywords(Set<String> words) {
+		StringBuilder buf = new StringBuilder();
+		for( String s : words ) {
+			if (invalidKeywords.contains(s.toLowerCase()))
+				continue;
+			if (buf.length() > 0) buf.append(", ");
+			buf.append(s);
+		}
+		return buf.toString();
+	}
+
+	public static String metaKeywords(Node node) {
+		return metaKeywords(node, true);
+	}
+
+	public static String metaKeywords(Node node, boolean includeMessage) {
+		Set<String> words = new LinkedHashSet<String>();
+		for( Node n = node; n!=null; n = n.getParent() ) {
+			metaKeywords(n.getSubjectHtml(),words);
+			if (includeMessage) {
+				metaKeywords(getFragment(n.getMessage().getText(), 200), words);
+				includeMessage = false;
+			}
+		}
+		if( node.getKind() == Node.Kind.APP ) {
+			String type = node.getType();
+			if (type.equals(Node.Type.GALLERY)) {
+				words.add("photo");
+				words.add("pictures");
+				words.add("gallery");
+				words.add("images");
+			} else if (type.equals(Node.Type.BLOG)) {
+				words.add("blog");
+			} else if (type.equals(Node.Type.NEWS)) {
+				words.add("news");
+				words.add("newspaper");
+				words.add("magazine");
+			} else {
+				words.add("forum");
+				words.add("board");
+				words.add("community");
+			}
+			if (node.getAssociatedMailingList() != null)
+				words.add("mailing list archive");
+		}
+		return metaKeywords(words);
+	}
+
+	public static String metaDescription(Node node) {
+		StringBuilder buf = new StringBuilder();
+		String name = node.getSubjectHtml();
+		buf.append(name);
+		if( node.getKind() == Node.Kind.APP ) {
+			String viewName = viewName(node).toLowerCase();
+			if (! (name.toLowerCase().indexOf(viewName) >= 0)) {
+				buf.append(' ').append(viewName);
+			}
+			if (node.getAssociatedMailingList()!=null)
+				buf.append(" and mailing list archive");
+			buf.append(".");
+		}
+		appendSnippet(node.getMessage().getText(), buf);
+		return buf.toString();
+	}
+
+	private static void appendSnippet(String text, StringBuilder buf) {
+		if (buf.length() >= 200 || text.length() == 0) return;
+		String fragment = getFragment(text, 200 - buf.length());
+		if (buf.charAt(buf.length() - 1) != '.') buf.append('.');
+		buf.append(' ');
+		buf.append(ModelHome.hideAllEmails(HtmlUtils.htmlEncode(fragment).replaceAll("\\s+"," ")));
+		if (fragment.length() < text.length()) buf.append("...");
+	}
+
+	private static String getFragment(String text, int size) {
+		if (text.length() <= size) return text;
+		int end = text.lastIndexOf(' ', size);
+		if (end < 0) end = size;
+		return text.substring(0, end);
+	}
+
+
+	public static String formatDateOnly(String label, Date date) {
+		return "<script>document.write('" + label + "' + Nabble.formatDateOnly(new Date("+date.getTime()+")));</script>";
+	}
+
+	public static String formatDateOnly(Date date) {
+		return "<script>document.write(Nabble.formatDateOnly(new Date("+date.getTime()+")));</script>";
+	}
+
+	public static String formatDateLong(String label, Date date) {
+		return "<script>document.write('" + label + "' + Nabble.formatDateLong(new Date("+date.getTime()+")));</script>";
+	}
+
+	public static String formatDateLong(Date date) {
+		return "<script>document.write(Nabble.formatDateLong(new Date("+date.getTime()+")));</script>";
+	}
+
+	public static String formatDateShort(Date date) {
+		return "<script>document.write(Nabble.formatDateShort(new Date("+date.getTime()+")));</script>";
+	}
+
+
+	private static final String jsWriteStart = "<script>document.write(";
+	private static final String jsWriteEnd = ");</script>";
+
+	public static String javascriptQuote(String s) {
+		StringBuilder buf = new StringBuilder();
+		buf.append( "'" );
+		int i = 0;
+		while(true) {
+			int i2 = s.indexOf(jsWriteStart,i);
+			if( i2 == -1 )
+				break;
+			int i3 = s.indexOf(jsWriteEnd,i2);
+			if( i3 == -1 )
+				throw new RuntimeException();
+			buf.append( HtmlUtils.javascriptStringEncode(s.substring(i,i2)) );
+			buf.append( "'+(" );
+			buf.append( s.substring(i2+jsWriteStart.length(),i3) );
+			buf.append( ")+'" );
+			i = i3 + jsWriteEnd.length();
+		}
+		buf.append( HtmlUtils.javascriptStringEncode(s.substring(i)) );
+		buf.append( "'" );
+		return buf.toString();
+	}
+
+	public static int getYear(Date date) {
+		return date.getYear() + 1900;
+	}
+
+	public static int thisYear() {
+		return getYear(new Date());
+	}
+
+	public static String capitalize(String s) {
+		return s.substring(0,1).toUpperCase() + s.substring(1);
+	}
+
+	public static void dontCache(HttpServletResponse response) {
+		response.setHeader("Cache-Control","no-cache, max-age=0");
+	}
+
+
+	public static Map<String,FileItem> getFileItems(HttpServletRequest request)
+		throws FileUploadException
+	{
+		DiskFileUpload fu = new DiskFileUpload();
+		Map<String,FileItem> map = new HashMap<String,FileItem>();
+		@SuppressWarnings("unchecked")
+		List<FileItem> fileItems = fu.parseRequest(request);
+		for( FileItem fi : fileItems ) {
+			map.put(fi.getFieldName(),fi);
+		}
+		return map;
+	}
+
+	public static String helpIndexUrl(HttpServletRequest request,HttpServletResponse response)
+		throws IOException
+	{
+		StringBuilder url = new StringBuilder();
+		url.append( request.getContextPath() );
+		url.append( "/help/Index.jtp" );
+		return url.toString();
+	}
+
+	public static long getLong(HttpServletRequest request,String param)
+		throws ServletException
+	{
+		return parseLong( request, request.getParameter(param) );
+	}
+
+	public static long parseLong(HttpServletRequest request,String s)
+		throws ServletException
+	{
+		try {
+			return Long.parseLong(s);
+		} catch(NumberFormatException e) {
+			if (invalidReferer(request) || s.contains(" Result: ")) {
+				logger.warn("Bad URL", e);
+				throw new MinorServletException(e);
+			} else
+				throw e;
+		}
+	}
+
+	public static int getInt(HttpServletRequest request,String param)
+		throws ServletException
+	{
+		return parseInt( request, request.getParameter(param) );
+	}
+
+	public static int parseInt(HttpServletRequest request,String s)
+		throws ServletException
+	{
+		try {
+			return Integer.parseInt(s);
+		} catch(NumberFormatException e) {
+			if (invalidReferer(request))
+				throw new ServletException(e);
+			else
+				throw e;
+		}
+	}
+
+	/*
+	The User-Agent should not be considered.  If a browser doesn't send the referer, then we have no way
+	to know if the referer is valid or not.  So we should assume it isn't to avoid having cases
+	where it really isn't valid go into error.log .  If the referer really was valid, then the same problem
+	should show up again in a good browser.  We shouldn't worry so much about taking care of broken browsers.
+	-fschmidt
+	*/
+	public static boolean invalidReferer(HttpServletRequest request) {
+		String referer = request.getHeader("referer");
+		if( referer == null
+			|| !referer.startsWith("http://" + request.getHeader("host"))
+		)
+			return true;
+		if( request.getMethod().equals("GET") ) {
+			StringBuffer url = request.getRequestURL();
+			String query = request.getQueryString();
+			if( query != null )
+				url.append( '?' ).append( query );
+			if( url.toString().equals(referer) )
+				return true;
+		}
+		return false;
+	}
+
+	public static ServletException servletException(HttpServletRequest request,String msg) {
+		if( invalidReferer(request) )
+			return new ServletException(msg);
+		throw new RuntimeException(msg);
+	}
+
+	public static String getString(HttpServletRequest request, String param)
+		throws ServletException
+	{
+		String value = request.getParameter(param);
+		if (value == null && invalidReferer(request)) {
+			throw new ServletException("Parameter is null: " + param + " [referer is null]");
+		}
+		return value;
+	}
+
+	// should be removed
+	public static String getReadAuthorizationKey(Node node) {
+		if( node==null )
+			return null;
+		node = Permissions.getPrivateNode(node);
+		return node!=null ? Long.toString(node.getId()) : null;
+	}
+
+	// should be removed
+	public static boolean authorizeForRead(String key,HttpServletRequest request,HttpServletResponse response) throws ServletException, IOException {
+		Node node = getSiteNotNull(request).getNode( Long.parseLong(key) );
+		User user = getUser(request,response);
+		if( user==null ) {
+			login("You must login to view " + node.getSubject(), request, response);
+			return false;
+		}
+
+		if( !Jtp.canBeViewedBy(node,user) ) {
+			response.sendRedirect(getUnauthorizedPath(node));
+			return false;
+		}
+		return true;
+	}
+
+	private static String getUnauthorizedPath(Node node) {
+		Template template = node.getSite().getTemplate( "unauthorized_path",
+			BasicNamespace.class, NabbleNamespace.class, NodeNamespace.class
+		);
+		StringWriter sw = new StringWriter();
+		template.run( new TemplatePrintWriter(sw), Collections.<String, Object>emptyMap(),
+			new BasicNamespace(template), new NabbleNamespace(node.getSite()), new NodeNamespace(Permissions.getPrivateNode(node))
+		);
+		return sw.toString();
+	}
+
+	private static final long startTime = System.currentTimeMillis()/1000*1000;
+
+	public static boolean cacheMe(HttpServletRequest request,HttpServletResponse response)
+		throws ServletException, IOException
+	{
+		if( startTime <= request.getDateHeader("If-Modified-Since") ) {
+			response.sendError(HttpServletResponse.SC_NOT_MODIFIED);
+			return true;
+		}
+		response.setDateHeader("Last-Modified",startTime);
+		return false;
+	}
+
+
+	public static void addBreadCrumbEvents(Collection<String> events,Node node) {
+		if( node==null ) return;
+		for( Node f = node.getApp(); f != null; f = f.getParent() ) {
+			events.add( Cache.nodeChangeEvent(f) );
+		}
+	}
+
+
+	public static List<Node> getPinnedChildren(Node parent) {
+		List<Node> pinned = new ArrayList<Node>();
+		NodeIterator<? extends Node> iter = parent.getChildren();
+		try {
+			while( iter.hasNext() ) {
+				Node node = iter.next();
+				if( !node.isPinned() )
+					break;
+				pinned.add(node);
+			}
+		} finally {
+			iter.close();
+		}
+		return pinned;
+	}
+
+	public static void addPinnedChild(Node parent, Node child) {
+		List<Node> pinned = getPinnedChildren(parent);
+		if (child.getKind() == Node.Kind.APP) {
+			// Insert after the last pinned forum (before pinned threads)
+			boolean added = false;
+			for (int i = 0; i < pinned.size(); i++) {
+				Node node = pinned.get(i);
+				if (node.getKind() == Node.Kind.POST) {
+					pinned.add(i, child);
+					added = true;
+					break;
+				}
+			}
+			if (!added)
+				pinned.add(child);
+		}
+		else {
+			pinned.add(child);
+		}
+		parent.pin(pinned.toArray(new Node[0]));
+	}
+
+	public static void unpinChild(Node parent, Node child) {
+		List<Node> pinned = getPinnedChildren(parent);
+		pinned.remove(child);
+		parent.pin(pinned.toArray(new Node[0]));
+	}
+
+
+	public static String snippet(Node node,int len) {
+		String text = node.getMessage().getTextWithoutQuotes();
+		return HtmlUtils.htmlEncode(ModelHome.hideAllEmails(NodeSearcher.getStartingFragment(text, len, "...")));
+	}
+
+
+	public static String noCid(String url) {
+		return url.replaceAll("(;|&)cid=(\\d|\\-)+", "");
+	}
+
+	// should go away  -fschmidt
+	public static String viewName(Node node) {
+		Node forum = node.getApp();
+		if (forum == null)
+			return "Forum";
+		String type = forum.getType();
+		if (type.equals(Node.Type.GALLERY))
+			return "Gallery";
+		else if (type.equals(Node.Type.NEWS))
+			return "Newspaper";
+		else if (type.equals(Node.Type.BLOG))
+			return "Blog";
+		else
+			return "Forum";
+	}
+
+	// should go away  -fschmidt
+	public static String childName(Node node, boolean plural) {
+		Node forum = node.getApp();
+		if (forum == null)
+			return plural? "Sub-Forums" : "Sub-Forum";
+		String type = forum.getType();
+		if (type.equals(Node.Type.GALLERY) ||
+			type.equals(Node.Type.NEWS) ||
+			type.equals(Node.Type.BLOG) ||
+			type.equals(Node.Type.BOARD))
+			return plural? "Subcategories" : "Subcategory";
+		else
+			return plural? "Sub-Forums" : "Sub-Forum";
+	}
+
+	public static String parentName(Node node) {
+		if (node.getParent() == null)
+			return "Current Parent";
+		return "Parent " + viewName(node.getParent());
+	}
+
+	// what do you plan for this?   -fschmidt
+	public static String getSmallLogo(String type) {
+		if (type.equals(Node.Type.GALLERY))
+			return "<img src=\"/images/homepage/gallery_sm.png\" width=20 height=15 alt=\"Free photo gallery\">";
+		else if (type.equals(Node.Type.BLOG))
+			return "<img src=\"/images/homepage/blog_sm.png\" width=20 height=17 alt=\"Free blog\">";
+		else if (type.equals(Node.Type.NEWS))
+			return "<img src=\"/images/homepage/news_sm.png\" width=20 height=15 alt=\"Free newspaper\">";
+		else
+			return "";
+	}
+
+	static String getCanonicalUrl(HttpServletRequest request) {
+		String current = ServletUtils.getCurrentURL(request);
+		if (current.startsWith(Lazy.homeContextUrl))
+			return null;
+		String host = request.getHeader("host");
+		return current.replace("http://"+host, Lazy.homeContextUrl);
+	}
+
+	private static final Pattern NUMBER_PATTERN = Pattern.compile("^[0-9]+$");
+	public static boolean isInteger(String s) {
+		return s != null && NUMBER_PATTERN.matcher(s).find();
+	}
+
+	/**
+	 * Maximum number of rows an app page can have.
+	 * This limit must exist because there are physical restrictions:
+	 *  - the $Js URL can't support too many nodes at the same time.
+	 *  - div tags have a limit of 32,768 pixels of height.
+	 */
+	public static int getMaxRowsPerPage(String type) {
+		int n = 100;
+		if (type.equals(Node.Type.BLOG))
+			n = 20;
+		return n;
+	}
+
+	/** Default number of rows an app page has. */
+	public static int getDefaultRowsPerPage(String type) {
+		int n = 35;
+		if (type.equals(Node.Type.BLOG))
+			n = 10;
+		else if (type.equals(Node.Type.GALLERY))
+			n = 12;
+		else if (type.equals(Node.Type.GALLERY))
+			n = 25;
+		return n;
+	}
+
+	public static int getDefaultMixedLength() { return 6;}
+	public static int getMaxMixedLength() { return 20;}
+
+	private static final String defaultHost = (String)Init.get("defaultHost");
+	static {
+		if( defaultHost==null ) {
+			logger.error("no defaultHost");
+			System.exit(-1);
+		}
+	}
+
+	public static String getDefaultHost() {
+		return defaultHost;
+	}
+
+	public static String getBaseUrl(HttpServletRequest request) {
+		String host = request.getHeader("host");
+		String scheme = request.getHeader("X-Forwarded-Proto");
+		if( scheme == null )
+			scheme = request.getScheme();
+		return scheme + "://" + host;
+	}
+
+	// fix jetty bug
+	public static void sendRedirect(HttpServletRequest request,HttpServletResponse response,String url)
+		throws IOException
+	{
+		if( url.startsWith("/") )
+			url = Jtp.getBaseUrl(request) + url;
+		response.sendRedirect(url);
+	}
+
+	private static class Lazy {
+		static final Pattern ROOT_URL_PATTERN;
+		static final String defaultContextUrl;
+		static final String homeContextUrl;
+		static {
+			defaultContextUrl = "http://" + defaultHost;
+			homeContextUrl = Init.get("homeContextUrl",defaultContextUrl);
+			ROOT_URL_PATTERN = Pattern.compile( ".*\\.(\\d+)\\."+Pattern.quote(defaultHost) );
+		}
+	}
+
+	public static String homePage() {
+		return nabble.view.web.Index.url();
+	}
+
+	public static String supportUrl() {
+		return "http://support.nabble.com/";
+	}
+
+	public static String supportLink() {
+		String supportUrl = supportUrl();
+		return supportUrl==null ? null : "<a href='"+supportUrl+"' target='_top'>Nabble Support</a>";
+	}
+
+	public static String defaultContextUrl() {
+		return Lazy.defaultContextUrl;
+	}
+
+	public static String homeContextUrl() {
+		return Lazy.homeContextUrl;
+	}
+
+	// doesn't return null
+	public static Site getSiteNotNull(HttpServletRequest request)
+		throws ServletException
+	{
+		Site site = getSite(request);
+		if( site == null )
+			throw servletException(request,"site not found");
+		return site;
+	}
+
+	private static final Object noSite = new Object();
+
+	public static Site getSite(HttpServletRequest request) {
+		Object obj = request.getAttribute("site");
+		if( obj == null ) {
+			Long siteId = getSiteIdFromDomain( ServletUtils.getHost(request) );
+			if( siteId != null )
+				obj = ModelHome.getSite(siteId);
+			if( obj == null )
+				obj = noSite;
+			request.setAttribute("root",obj);
+		}
+		return obj==noSite ? null : (Site)obj;
+	}
+
+	public static Site getSiteFromUrl(String url) {
+		String domain = extractDomain(url);
+		if( domain == null )
+			return null;
+		Long siteId = getSiteIdFromDomain(domain);
+		if( siteId == null )
+			return null;
+		return ModelHome.getSite(siteId);
+	}
+
+	public static Long getSiteIdFromDomain(String domain) {
+		if( domain.equals(defaultHost) )
+			return null;
+		Matcher m = Lazy.ROOT_URL_PATTERN.matcher(domain);
+		if( m.matches() ) {
+			return Long.parseLong(m.group(1));
+		} else {
+			return ModelHome.getSiteIdFromDomain(domain);
+		}
+	}
+
+	public static String getDefaultBaseUrl(Site site) {
+		return ViewUtils.getDefaultBaseUrl( site.getId(), site.getRootNode().getSubject(), defaultHost );
+	}
+
+	public static String extractDomain(String url) {
+		int posDoubleSlash = url.indexOf("//");
+		int posDomainEnd = url.indexOf('/', posDoubleSlash+2);
+		// if the last slash was not found, we can assume the domain ends at the end of the string.
+		posDomainEnd = posDomainEnd == -1? url.length() : posDomainEnd;
+		return posDoubleSlash > 0 && posDoubleSlash < 8 && posDomainEnd > posDoubleSlash? url.substring(posDoubleSlash+2, posDomainEnd) : null;
+	}
+
+	// permissions hacks
+
+	private static boolean can(Person person,String macro,Node node) {
+		Template template = node.getSite().getTemplate( macro,
+			BasicNamespace.class, NabbleNamespace.class, UserNamespace.class
+		);
+		StringWriter sw = new StringWriter();
+		Map<String,Object> args = new HashMap<String,Object>();
+		args.put( "node_attr", new NodeNamespace(node) );
+		template.run( new TemplatePrintWriter(sw), args,
+			new BasicNamespace(template), new NabbleNamespace(node.getSite()), new UserNamespace(person)
+		);
+		return sw.toString().equals("true");
+	}
+
+	public static boolean canBeEditedBy(Node node,Person person) {
+		return can(person,"can_edit",node);
+	}
+
+	public static boolean canBeRemovedBy(Node node,Person person) {
+		return can(person,"can_move",node);
+	}
+
+	public static boolean canBeDeletedBy(Node node,Person person) {
+		return can(person,"can_delete",node);
+	}
+
+	public static boolean canBeViewedBy(Node node,Person person) {
+		return can(person,"can_view",node);
+	}
+
+	public static boolean canMove(Node node,Person person) {
+		return can(person,"can_move",node);
+	}
+
+	public static boolean canAssign(Node node,Person person) {
+		return can(person,"can_be_assigned_to",node);
+	}
+
+	public static boolean canChangePostDateOf(Node node,Person person) {
+		return can(person,"can_change_post_date_of",node);
+	}
+
+	public static boolean isSiteAdmin(Site site,Person person) {
+		return person instanceof User && site.getRootNode().getOwner().equals(person);  // for now
+	}
+
+	public static final String CACHED = "cached";
+
+	public static boolean isCached(HttpServletRequest request,HttpServletResponse response) {
+		return response.containsHeader("Etag") || request.getAttribute(CACHED) != null;
+	}
+
+	public static String termsUrl(boolean back) {
+		return homeContextUrl() + "/Terms.jtp";
+	}
+}