view src/nabble/view/lib/Jtp.java @ 47:72765b66e2c3

remove mailing list code
author Franklin Schmidt <fschmidt@gmail.com>
date Fri, 18 Jun 2021 17:44:24 -0600
parents 18cf4872fd7f
children
line wrap: on
line source

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 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");
			}
		}
		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);
			}
			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);
		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(User 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,User person) {
		return can(person,"can_edit",node);
	}

	public static boolean canBeRemovedBy(Node node,User person) {
		return can(person,"can_move",node);
	}

	public static boolean canBeDeletedBy(Node node,User person) {
		return can(person,"can_delete",node);
	}

	public static boolean canBeViewedBy(Node node,User person) {
		return can(person,"can_view",node);
	}

	public static boolean canMove(Node node,User person) {
		return can(person,"can_move",node);
	}

	public static boolean canAssign(Node node,User person) {
		return can(person,"can_be_assigned_to",node);
	}

	public static boolean canChangePostDateOf(Node node,User person) {
		return can(person,"can_change_post_date_of",node);
	}

	public static boolean isSiteAdmin(Site site,User person) {
		return 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";
	}
}