view src/nabble/model/UserImpl.java @ 51:f88ed76ca757

add back noArchive
author Franklin Schmidt <fschmidt@gmail.com>
date Fri, 02 Jul 2021 00:32:31 -0600
parents 72765b66e2c3
children 7df8ec497281
line wrap: on
line source

/*

Copyright (C) 2003  Franklin Schmidt <frank@gustos.com>

*/

package nabble.model;

import fschmidt.db.DbDatabase;
import fschmidt.db.DbNull;
import fschmidt.db.DbObjectFactory;
import fschmidt.db.DbRecord;
import fschmidt.db.DbTable;
import fschmidt.db.DbUtils;
import fschmidt.db.Listener;
import fschmidt.db.ListenerList;
import fschmidt.db.LongKey;
import fschmidt.db.postgres.DbDatabaseImpl;
import fschmidt.util.java.Computable;
import fschmidt.util.java.Memoizer;
import fschmidt.util.java.ObjectUtils;
import fschmidt.util.java.SimpleCache;
import fschmidt.util.java.TimedCacheMap;
import fschmidt.util.mail.MailAddress;
import org.jasypt.digest.PooledStringDigester;
import org.jasypt.salt.FixedByteArraySaltGenerator;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.awt.image.BufferedImage;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Date;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.WeakHashMap;
import java.util.concurrent.CopyOnWriteArrayList;


final class UserImpl extends PersonImpl implements User {
	private static final Logger logger = LoggerFactory.getLogger(UserImpl.class);

	final SiteKey siteKey;
	private final DbRecord<LongKey,UserImpl> record;
	private String email;
	private String passwordDigest;
	private String name;
	private Date registered;
	private boolean noArchive;
	private Message signature = null;
	private int bounces;

	private UserImpl(SiteKey siteKey,LongKey key,ResultSet rs)
		throws SQLException
	{
		this.siteKey = siteKey;
		record = table(siteKey).newRecord(this,key);
		email = rs.getString("email");
		passwordDigest = rs.getString("password_digest");
		name = rs.getString("name");
		registered = DbUtils.getDate(rs,"registered");
		noArchive = rs.getBoolean("no_archive");
		String signatureRaw = rs.getString("signature");
		String signatureFormatS = rs.getString("signature_format");
		if( signatureRaw!=null && signatureFormatS!=null ) {
			Message.Format signatureFormat = Message.Format.getMessageFormat( signatureFormatS.charAt(0) );
			signature = new Message(signatureRaw,signatureFormat);
		}
		bounces = rs.getInt("bounces");
		for( ExtensionFactory<User,?> factory : extensionFactories ) {
			Object obj = factory.construct(this,rs);
			if( obj != null )
				getExtensionMap().put(factory,obj);
		}
	}

	private UserImpl(SiteImpl site) {
		this.siteKey = site.siteKey;
		record = table(siteKey).newRecord(this);
	}


	public DbRecord<LongKey,UserImpl> getDbRecord() {
		return record;
	}

	private DbTable<LongKey,UserImpl> table() {
		return record.getDbTable();
	}

	private DbDatabase db() {
		return table().getDbDatabase();
	}

	public long getId() {
		return record.getPrimaryKey().value();
	}

	SiteImpl getSiteImpl() {
		return siteKey.site();
	}

	public Site getSite() {
		return getSiteImpl();
	}

	public boolean isDeactivated() {
		return !isRegistered() && noArchive;
	}

	private void setNoArchive(boolean noArchive) {
		if( this.noArchive == noArchive )
			return;

		if( !db().isInTransaction() ) {
			db().beginTransaction();
			try {
				UserImpl user = DbUtils.getGoodCopy(this);
				user.setNoArchive(noArchive);
				user.getDbRecord().update();
				db().commitTransaction();
				return;
			} finally {
				db().endTransaction();
			}
		}
		this.noArchive = noArchive;
		record.fields().put("no_archive",DbNull.fix(noArchive));
	}

	public String getEmail() {
		return email;
	}

	static void validateEmail(String email) throws ModelException.EmailFormat {
		if (!new MailAddress(email).isValid()) {
			throw new ModelException.EmailFormat(email);
		}
	}

	public void setEmail(String email) throws ModelException {
		if( !db().isInTransaction() ) {
			db().beginTransaction();
			try {
				UserImpl user = DbUtils.getGoodCopy(this);
				user.setEmail(email);
				user.getDbRecord().update();
				db().commitTransaction();
				return;
			} finally {
				db().endTransaction();
			}
		}
		validateEmail(email);
		setEmail2(email);
	}

	private void setEmail2(String email) throws ModelException {
		if( email.equals(this.email) )
			return;
		SiteImpl site = getSiteImpl();
		if( site.getUserImplFromEmail(email) != null )
			throw ModelException.newInstance("email_already_in_user","Email already in use");
		this.email = email;
		record.fields().put("email",email);
	}

	public String getPasswordDigest() {
		return passwordDigest;
	}

	public void setPassword(String password) throws ModelException {
		if( "".equals(password) )
			throw ModelException.newInstance("empty_password","Password cannot be empty");
		setPasswordDigest(digestPassword(password));
	}

	public void setPasswordDigest(String passwordDigest) {
		if( ObjectUtils.equals(passwordDigest,this.passwordDigest) )
			return;
		this.passwordDigest = passwordDigest;
		record.fields().put("password_digest",DbNull.fix(passwordDigest));
		synchronized (passcookieLock) {
			this.passcookie = null;
		}
	}

	private volatile String passcookie = null;
	private Object passcookieLock = new Object();

	public String getPasscookie() {
		String p = passcookie;
		if (p==null) {
			synchronized (passcookieLock) {
				p = passcookie;
				if (p==null) {
					p = calcPasscookie();
					passcookie = p;
				}
			}
		}
		return p;
	}

	public String getName() {
		return name;
	}

	public void setName(String name) throws ModelException {
		setName(name,true);
	}

	private void setName(String name,boolean replaceUnregistered) throws ModelException {
		name = name.trim();
		if( name.equals("") )
			throw ModelException.newInstance("empty_user_name","User name cannot be empty.");
		if( name.equals(this.name) )
			return;
		if( !name.equalsIgnoreCase(this.name) ) {
			UserImpl user = getSiteImpl().getUserImplFromName(name);
			if( user != null ) {
				if( !replaceUnregistered || user.isRegistered() )
					throw ModelException.newInstance("user_name_already_in_use","User name '"+name+"' already in use");
				user.setNameLike2(name);
				user.update();
			}
			try {
				Connection con = db().getConnection();
				PreparedStatement stmt = con.prepareStatement(
					"select 'x' from registration where email!=? and name=?"
				);
				stmt.setString(1,this.email);
				stmt.setString(2,name);
				try {
					if( stmt.executeQuery().next() )
						throw ModelException.newInstance("user_name_already_in_use","User name '"+name+"' already in use");
				} finally {
					stmt.close();
					con.close();
				}
			} catch(SQLException e) {
				throw new RuntimeException(e);
			}
		}
		this.name = name;
		record.fields().put("name",name);
	}

	void setNameLike(String name,boolean replaceUnregistered) {
		try {
			setName(name,replaceUnregistered);
		} catch(ModelException e) {
			setNameLike2(name);
		}
	}

	private void setNameLike2(String name) {
		for( int i=2; true; i++ ) {
			try {
				setName(name+"-"+i,false);
				break;
			} catch(ModelException e2) {}
		}
	}

	/* To be called from the shell */
	public void changeNameTo(String newName) {
		db().beginTransaction();
		try {
			UserImpl u = (UserImpl) getGoodCopy();
			u.setName(newName);
			u.update();
			db().commitTransaction();
			DbUtils.uncache(u);
		} catch (ModelException e) {
			throw new RuntimeException(e);
		} finally {
			db().endTransaction();
		}
	}

	public Date getRegistered() {
		return registered;
	}

	void setRegistered(Date registered) {
		if( ObjectUtils.equals(registered,this.registered) )
			return;
		this.registered = registered;
		record.fields().put("registered",DbNull.fix(registered));
	}

	public boolean equals(Object obj) {
		return obj instanceof User && ((User)obj).getId()==getId();
	}

	public int hashCode() {
		return (int)getId();
	}

	public String toString() {
		return record.isInDb() ? "user-"+getId() : "user-new";
	}

	public void register() throws ModelException {
		register(new Date());
	}

	public void register(Date registerDate) throws ModelException {
		if( !db().isInTransaction() ) {
			db().beginTransaction();
			try {
				UserImpl user;
				if( record.isInDb() ) {
					user = DbUtils.getGoodCopy(this);
					user.setEmail(email);
					user.setName(name);
					user.setPasswordDigest(passwordDigest);
				} else {
					user = this;
				}
				user.register();
				db().commitTransaction();
			} finally {
				db().endTransaction();
			}
			return;
		}
		if( passwordDigest==null )
			throw new RuntimeException();
		setRegistered( registerDate );
		if( record.isInDb() ) {
			record.update();
		} else {
			insert();
		}
	}

	public boolean isRegistered() {
		return record.isInDb() && registered!=null;
	}

	void insert() {
		if( email==null || name==null )
			throw new RuntimeException();
		record.insert();
	}

	public void update() {
		if( !db().isInTransaction() )
			throw new RuntimeException("this should be done in a transaction");
		Set<String> keys = record.fields().keySet();
		if( keys.contains("name") || keys.contains("signature") ) {
			getSiteImpl().update();  // fire change listeners
		}
		getDbRecord().update();
	}

	public User getGoodCopy() {
		return DbUtils.getGoodCopy(this);
	}




	public int getExternalHash(String url) {
		return (url.toLowerCase() + getId()).hashCode();
	}


	static final ListenerList<UserImpl> preUpdateListeners = new ListenerList<UserImpl>();
	static final ListenerList<UserImpl> postInsertListeners = new ListenerList<UserImpl>();

	private static Computable<SiteKey,DbTable<LongKey,UserImpl>> tables = new SimpleCache<SiteKey,DbTable<LongKey,UserImpl>>(new WeakHashMap<SiteKey,DbTable<LongKey,UserImpl>>(), new Computable<SiteKey,DbTable<LongKey,UserImpl>>() {
		public DbTable<LongKey,UserImpl> get(SiteKey siteKey) {
			DbDatabase db = siteKey.getDb();
			final long siteId = siteKey.getId();
			DbTable<LongKey,UserImpl> table = db.newTable("user_",db.newIdentityLongKeySetter("user_id")
				, new DbObjectFactory<LongKey,UserImpl>() {
					public UserImpl makeDbObject(LongKey key,ResultSet rs,String tableName)
						throws SQLException
					{
						SiteKey siteKey = SiteKey.getInstance(siteId);
						return new UserImpl(siteKey,key,rs);
					}
				}
			);
			table.getPreUpdateListeners().add(preUpdateListeners);
			table.getPostInsertListeners().add(postInsertListeners);
			return table;
		}
	});

	private static DbTable<LongKey,UserImpl> table(SiteKey siteKey) {
		return tables.get(siteKey);
	}

	static UserImpl getUser(SiteKey siteKey,long id) {
		UserImpl user = table(siteKey).findByPrimaryKey(new LongKey(id));
		if( user==null )
			logger.warn("user "+id+" not found");
		return user;
	}

	static Collection<UserImpl> getUsers(SiteKey siteKey,Collection<Long> ids) {
		List<LongKey> list = new ArrayList<LongKey>();
		for( long id : ids ) {
			list.add( new LongKey(id) );
		}
		return table(siteKey).findByPrimaryKey(list).values();
	}

	static UserImpl getUser(SiteKey siteKey,ResultSet rs)
		throws SQLException
	{
		return table(siteKey).getDbObject(rs);
	}

	static void getUsers(SiteKey siteKey,PreparedStatement stmt,List<? super UserImpl> list)
		throws SQLException
	{
		ResultSet rs = stmt.executeQuery();
		while( rs.next() ) {
			UserImpl user = getUser(siteKey,rs);
			list.add(user);
		}
		rs.close();
		stmt.close();
	}

	static List<UserImpl> getUsers(SiteKey siteKey,PreparedStatement stmt)
		throws SQLException
	{
		List<UserImpl> list = new ArrayList<UserImpl>();
		getUsers(siteKey,stmt,list);
		return list;
	}

	private static UserImpl getUser(SiteImpl site,String val,String sql) {
		try {
			SiteKey siteKey = site.siteKey;
			Connection con = siteKey.getDb().getConnection();
			PreparedStatement stmt = con.prepareStatement(sql);
			stmt.setString(1,val);
			ResultSet rs = stmt.executeQuery();
			UserImpl user = rs.next() ? getUser(siteKey,rs) : null;
			rs.close();
			stmt.close();
			con.close();
			return user;
		} catch(SQLException e) {
			throw new RuntimeException(e);
		}
	}

	static UserImpl getUserFromEmail(SiteImpl site,String email) {
		return getUser( site, email.toLowerCase(),
			"select * from user_"
			+" where lower(email)=?"
		);
	}

	static UserImpl getUserFromName(SiteImpl site,String name) {
		return getUser( site, name.toLowerCase(),
			"select * from user_"
			+" where lower(name)=?"
		);
	}

	static UserImpl createGhost(SiteImpl site,String email) {
		UserImpl user = new UserImpl(site);
		try {
			user.setEmail2(email);
		} catch(ModelException e) {
			throw new RuntimeException(e);
		}
		return user;
	}

	// Subscriptions -----------------------------------------------------------

	public boolean isSubscribed(Node node) {
		return SubscriptionImpl.isSubscribed(this, (NodeImpl) node);
	}

	public Subscription getSubscription(Node node) {
		return SubscriptionImpl.getSubscription( this, (NodeImpl)node );
	}

	public Subscription subscribe(Node node,Subscription.To to,Subscription.Type type) {
		clearBounces();
		Subscription subscription = getSubscription(node);
		if( subscription != null ) {
			subscription.setTo(to);
			subscription.setType(type);
			return subscription;
		} else {
			return SubscriptionImpl.insert( this, (NodeImpl)node, to, type );
		}
	}

	/*10 posts in 5 minutes */
	private static final RecentPostLimit postLimit1 = new RecentPostLimit(5 * 60 * 1000L, 10);

	/* 30 posts in 15 minutes */
	private static final RecentPostLimit postLimit2 = new RecentPostLimit(15 * 60 * 1000L, 30);

	void updateNewPostLimit() {
		String key = siteKey.getId() + "-" + record.getPrimaryKey().value();
		postLimit1.insert(key);
		postLimit2.insert(key);
	}

	public boolean hasTooManyPosts() {
		String key = siteKey.getId() + "-" + record.getPrimaryKey().value();
		return postLimit1.hasTooManyPosts(key) || postLimit2.hasTooManyPosts(key);
	}

	private static class RecentPostLimit {
		private final long timeLimit;
		private final int postLimit;
		private final Map<String,long[]> floodMap;

		private RecentPostLimit(long timeLimit, int postLimit) {
			this.timeLimit = timeLimit;
			this.postLimit = postLimit;
			this.floodMap = new TimedCacheMap<String,long[]>(timeLimit);
		}

		public void insert(String key) {
			long[] recentPostTimes;
			synchronized(floodMap) {
				recentPostTimes = floodMap.get(key);
				if( recentPostTimes==null ) {
					recentPostTimes = new long[postLimit];
					floodMap.put(key,recentPostTimes);
				}
			}
			long now = System.currentTimeMillis();
			long recently = now - timeLimit;
			synchronized(recentPostTimes) {
				for( int i=0; i<recentPostTimes.length; i++ ) {
					if( recentPostTimes[i] < recently ) {
						recentPostTimes[i] = now;
						return;
					}
				}
			}
		}

		public boolean hasTooManyPosts(String key) {
			long[] recentPostTimes;
			synchronized(floodMap) {
				recentPostTimes = floodMap.get(key);
				if (recentPostTimes==null)
					return false;
			}
			long now = System.currentTimeMillis();
			long recently = now - timeLimit;
			synchronized(recentPostTimes) {
				for (long time : recentPostTimes) {
					if (time < recently) {
						return false;
					}
				}
			}
			return true;
		}
	}


	static UserImpl getOrCreateUnregisteredUser(SiteImpl site,String email,String name)
		throws ModelException
	{
		DbDatabase db = site.getDb();
		if( !db.isInTransaction() ) {
			db.beginTransaction();
			try {
				UserImpl user = getOrCreateUnregisteredUser(site,email,name);
				db.commitTransaction();
				return user;
			} finally {
				db.endTransaction();
			}
		}
		UserImpl user = site.getUserImplFromEmail(email);
		if( user==null ) {
			user = new UserImpl(site);
			user.setEmail(email);
		} else {
			if( user.isRegistered() )
				throw ModelException.newInstance("email_already_registered","This email is already registered");
			validateEmail(user.getEmail());
		}
		user.setName(name);
		if( !user.record.isInDb() ) {
			user.insert();
		} else if( !user.record.fields().isEmpty() ) {
			user.update();
		}
		return user;
	}

	// registration

	static UserImpl createUser(SiteImpl site,String email,String password,String name) throws ModelException {
		return createUser2(site, email, digestPassword(password), name);
	}

	private static UserImpl createUser2(SiteImpl site,String email,String passwordDigest,String name) throws ModelException {
		// transaction used because setName() may update user
		DbDatabase db = site.getDb();
		if( !db.isInTransaction() ) {
			db.beginTransaction();
			try {
				UserImpl user = createUser2(site,email,passwordDigest,name);
				db.commitTransaction();
				return user;
			} finally {
				db.endTransaction();
			}
		}
		if (!new MailAddress(email).isValid()) {
			throw new ModelException.EmailFormat("invalid_email");
		}
		UserImpl user = site.getUserImplFromEmail(email);
		if( user==null ) {
			user = new UserImpl(site);
			user.setEmail(email);
		} else {
			if( user.isRegistered() )
				throw ModelException.newInstance("user_already_registered","User is already registered");
			validateEmail(user.getEmail());
		}
		user.setPasswordDigest(passwordDigest);
		user.setName(name);
		return user;
	}

	static UserImpl getOrCreateUser(SiteImpl site,String email) {
		UserImpl user = site.getUserImplFromEmail(email);
		if (user == null) {
			String username = email.substring(0, email.indexOf('@'));
			user = createGhost(site,email);
			user.setNameLike(username, false);
			user.insert();
		}
		return user;
	}

	private static final Object regLock = new Object();

	String newRegistration(String nextUrl) {
		if( nextUrl.equals("null") )
			throw new RuntimeException("nextUrl is \"null\"");
		synchronized(regLock) {
			String key;
			try {
				Connection con = db().getConnection();
				{
					PreparedStatement stmt = con.prepareStatement(
						"select 'x' from registration where key_=?"
					);
					do {
						key = Double.toString(Math.random());
						stmt.setString(1,key);
					} while( stmt.executeQuery().next() );
					stmt.close();
				}
				{
					PreparedStatement stmt = con.prepareStatement(
						"insert into registration"
						+" ( key_, email, password_digest, name, next_url ) values (?,?,?,?,?)"
					);
					int i = 0;
					stmt.setString(++i,key);
					stmt.setString(++i,getEmail());
					stmt.setString(++i,getPasswordDigest());
					stmt.setString(++i,getName());
					stmt.setString(++i,nextUrl);
					stmt.executeUpdate();
					stmt.close();
				}
				{
					Statement stmt = con.createStatement();
					stmt.executeUpdate(
						"delete from registration where date_<" + Db.arcana.dateSub("now()",7,"day")
					);
					stmt.close();
				}
				con.close();
			} catch(SQLException e) {
				throw new RuntimeException(e);
			}
			return key;
		}
	}

	static User getRegistration(SiteImpl site,String registrationKey)
		throws ModelException
	{
		try {
			DbDatabase db = site.getDb();
			Connection con = db.getConnection();
			PreparedStatement stmt = con.prepareStatement(
				"select * from registration where key_=?"
			);
			stmt.setString(1,registrationKey);
			ResultSet rs = stmt.executeQuery();
			try {
				if( !rs.next() )
					return null;
				String email = rs.getString("email");
				String passwordDigest = rs.getString("password_digest");
				String name = rs.getString("name");
				return createUser2(site,email,passwordDigest,name);
			} finally {
				rs.close();
				stmt.close();
				con.close();
			}
		} catch(SQLException e) {
			throw new RuntimeException(e);
		}
	}

	static String getNextUrl(SiteKey siteKey,String registrationKey)
	{
		try {
			Connection con = siteKey.getDb().getConnection();
			PreparedStatement stmt = con.prepareStatement(
				"select next_url from registration where key_=?"
			);
			stmt.setString(1,registrationKey);
			ResultSet rs = stmt.executeQuery();
			try {
				if( !rs.next() )
					return null;
				return rs.getString("next_url");
			} finally {
				rs.close();
				stmt.close();
				con.close();
			}
		} catch(SQLException e) {
			throw new RuntimeException(e);
		}
	}

	// Called from beanshell
	private static void deletePendingRegistration(Site site,String email, String username) {
		try {
			Connection con = site.getDb().getConnection();
			PreparedStatement stmt = con.prepareStatement(
				"delete from registration where email=? or name = ?"
			);
			stmt.setString(1,email);
			stmt.setString(2,username);
			stmt.executeUpdate();
			stmt.close();
			con.close();
		} catch(SQLException e) {
			throw new RuntimeException(e);
		}
	}

	public void deactivate() {
		db().beginTransaction();
		try {
			UserImpl user = DbUtils.getGoodCopy(this);
			user.setNoArchive(true);
			user.setRegistered(null);
			user.setPasswordDigest(null);
			user.record.update();
			db().commitTransaction();
			logger.info("User removed his/her account: " + getEmail());
		} finally {
			db().endTransaction();
		}
	}

	private DbParamSetter simpleParamSetter() {
		return new DbParamSetter() {
			public void setParams(PreparedStatement stmt) throws SQLException {
				stmt.setLong( 1, getId() );
			}
		};
	}

	public Message getSignature() {
		return signature;
	}

	public User setSignature( String signatureRaw, Message.Format signatureFormat )
		throws ModelException
	{
		if( !db().isInTransaction() ) {
			db().beginTransaction();
			try {
				UserImpl user = DbUtils.getGoodCopy(this);
				user.setSignature(signatureRaw,signatureFormat);
				user.getDbRecord().update();
				db().commitTransaction();
				return DbUtils.getGoodCopy(user);
			} finally {
				db().endTransaction();
			}
		}
		if( signatureRaw==null || signatureRaw.trim().length()==0 ) {
			if( signature != null ) {
				signature = null;
				record.fields().put("signature",DbNull.STRING);
				record.fields().put("signature_format",DbNull.STRING);
			}
		} else {
			Message newSignature = new Message(signatureRaw,signatureFormat);
			if( !newSignature.equals(signature) ) {
				signature = newSignature;
				record.fields().put("signature",signatureRaw);
				record.fields().put("signature_format",Character.toString(signatureFormat.getCode()));
			}
		}
		return this;
	}


	public void saveAvatar(BufferedImage smallImage,BufferedImage bigImage) throws ModelException {
		if( !db().isInTransaction() ) {
			db().beginTransaction();
			try {
				DbUtils.getGoodCopy(this).saveAvatar(smallImage,bigImage);
				db().commitTransaction();
			} finally {
				db().endTransaction();
			}
			return;
		}
		Message.AvatarSource as = new Message.AvatarSource(this);
		FileUpload.saveImage(smallImage,ModelHome.AVATAR_SMALL,as);
		FileUpload.saveImage(bigImage,ModelHome.AVATAR_BIG,as);
		getSiteImpl().update();  // fire change listeners
		DbUtils.uncache(this);
	}

	public void deleteAvatar() {
		if( !db().isInTransaction() ) {
			db().beginTransaction();
			try {
				DbUtils.getGoodCopy(this).deleteAvatar();
				db().commitTransaction();
			} finally {
				db().endTransaction();
			}
			return;
		}
		final Message.AvatarSource as = new Message.AvatarSource(this);
		FileUpload.deleteFile(ModelHome.AVATAR_SMALL,as);
		FileUpload.deleteFile(ModelHome.AVATAR_BIG,as);
		getSiteImpl().update();  // fire change listeners
		db().runAfterCommit(new Runnable(){public void run(){
			FileUpload.fireFileUpdateListeners(as);
		}});
		DbUtils.uncache(this);
	}

	private boolean hasAvatar;
	private boolean checkedAvatar = false;

	public synchronized boolean hasAvatar() {
		if( !checkedAvatar ) {
			Message.AvatarSource as = new Message.AvatarSource(this);
			hasAvatar = FileUpload.hasFile(as,ModelHome.AVATAR_SMALL) && FileUpload.hasFile(as,ModelHome.AVATAR_BIG);
			checkedAvatar = true;
		}
		return hasAvatar;
	}



	public Node newRootNode(Node.Kind kind,String subject,String message,Message.Format msgFmt,Site site,String type) throws ModelException {
		return NodeImpl.newRootNode(kind,this,subject,message,msgFmt,(SiteImpl)site,type);
	}

	public Node newChildNode(Node.Kind kind,String subject,String message,Message.Format msgFmt,Node parent) throws ModelException {
		return NodeImpl.newChildNode(kind,this,subject,message,msgFmt,(NodeImpl)parent);
	}

	public String getSearchId() {
		return Long.toString(getId());
	}

	public String getIdString() {
		return Long.toString(getId());
	}

	private void clearBounces() {
		if( bounces==0 )
			return;
		bounces = 0;
		record.fields().put("bounces",DbNull.INTEGER);
		record.update();
	}

	void bounced() {
		record.fields().put("bounces",++bounces);
		record.update();
	}

	int getBounces() {
		return bounces;
	}

	private static final int bounceLimit = Init.get("bounceLimit",100);

	boolean isAutoUnsubscribe() {
		return isDeactivated() || bounces > bounceLimit;
	}




	private volatile Map<String, Integer> nodeCount = new HashMap<String, Integer>();

	public final int getNodeCount(String cnd) {
		String key = cnd == null? "none" : cnd;
		if (!nodeCount.containsKey(key)) {
			try {
				Connection con = db().getConnection();
				PreparedStatement stmt = con.prepareStatement(
					"select count(*) as n from node where owner_id = ?" +
					(cnd == null? "" : " and " + cnd)
				);
				stmt.setLong(1,getId());
				ResultSet rs = stmt.executeQuery();
				rs.next();
				nodeCount.put(key, rs.getInt("n"));
				rs.close();
				stmt.close();
				con.close();
			} catch(SQLException e) {
				throw new RuntimeException(e);
			}
		}
		return nodeCount.get(key);
	}

	void setNodeCount(int nodeCount) {
		this.nodeCount.put("none", nodeCount);
	}

	static {
		Listener<NodeImpl> listener = new Listener<NodeImpl>() {
			public void event(NodeImpl node) {
				table(node.siteKey).uncache(new LongKey(node.getOwnerId()));
			}
		};
		NodeImpl.postInsertListeners.add(listener);
		NodeImpl.postDeleteListeners.add(listener);
	}


	public void moveToRegisteredAccount(final String cookie) {
		List<NodeImpl> nodes = new CursorNodeIterator( siteKey,
				"select * from node where cookie=?"
			,
				new DbParamSetter() {
					public void setParams(PreparedStatement stmt) throws SQLException {
						stmt.setString(1,cookie);
					}
				}
		).asList();
		for( NodeImpl n : nodes ) {
			n.setOwner(this);
			n.update();
		}
	}


	public NodeIterator<? extends Node> getNodesByDateDesc(String cnd) {
		return new CursorNodeIterator( siteKey,
				"select * from node where owner_id = ?" +
				(cnd == null? "" : " and " + cnd) +
				" order by when_created desc"
			,
				new DbParamSetter() {
					public void setParams(PreparedStatement stmt) throws SQLException {
						stmt.setLong( 1, getId() );
					}
				}
		);
	}


	public int deleteNodes() {
		List<NodeImpl> nodes = new CursorNodeIterator( siteKey,
				"select *"
				+" from node"
				+" where owner_id = ?"
			, simpleParamSetter()
		).asList();
		int n = 0;
		for( NodeImpl node : nodes ) {
			db().beginTransaction();
			try {
				DbUtils.getGoodCopy(node).deleteMessageOrNode();
				db().commitTransaction();
				n++;
			} finally {
				db().endTransaction();
			}
		}
		return n;
	}

	public int deleteNodesRecursively() {
		List<NodeImpl> nodes = new CursorNodeIterator( siteKey,
				"select *"
				+" from node"
				+" where owner_id = ?"
			, simpleParamSetter()
		).asList();
		int n = 0;
		for( NodeImpl node : nodes ) {
			db().beginTransaction();
			try {
				DbUtils.getGoodCopy(node).deleteRecursively();
				db().commitTransaction();
				n++;
			} finally {
				db().endTransaction();
			}
		}
		return n;
	}


	private Map<ExtensionFactory<User,?>,Object> extensionMap;

	private synchronized Map<ExtensionFactory<User, ?>, Object> getExtensionMap() {
		if (extensionMap == null)
			extensionMap = new HashMap<ExtensionFactory<User, ?>, Object>();
		return extensionMap;
	}

	public <T> T getExtension(ExtensionFactory<User,T> factory) {
		synchronized(getExtensionMap()) {
			Object obj = extensionMap.get(factory);
			if( obj == null ) {
				obj = factory.construct(this);
				if( obj != null )
					extensionMap.put(factory,obj);
			}
			return factory.extensionClass().cast(obj);
		}
	}

	private static Collection<ExtensionFactory<User,?>> extensionFactories = new CopyOnWriteArrayList<ExtensionFactory<User,?>>();

	static <T> void addExtensionFactory(ExtensionFactory<User,T> factory) {
		extensionFactories.add(factory);
		Db.clearCache();
	}




	// visited node

	private final Map<Long,Long> visitedNodeCache = new HashMap<Long,Long>();

	public Long lastVisitedNodeId(long nodeId) {
		synchronized(visitedNodeCache) {
			return visitedNodeCache.containsKey(nodeId) ? visitedNodeCache.get(nodeId) : lastVisitedNodeIds(Collections.singletonList(nodeId)).get(nodeId);
		}
	}

	public Map<Long,Long> lastVisitedNodeIds(Collection<Long> nodeIds) {
		synchronized(visitedNodeCache) {
			Set<Long> notCached = new HashSet<Long>();
			for( Long nodeId : nodeIds ) {
				if( !visitedNodeCache.containsKey(nodeId) )
					notCached.add(nodeId);
			}
			if( !notCached.isEmpty() ) {
				StringBuilder sql = new StringBuilder();
				sql
					.append( "select node_id, last_node_id from visited where user_id = " )
					.append( getId() )
					.append( " and node_id in (" )
				;
				Iterator<Long> iter = notCached.iterator();
				sql.append( iter.next() );
				while( iter.hasNext() ) {
					sql.append( ',' ).append( iter.next() );
				}
				sql.append( ")" );
				try {
					Connection con = db().getConnection();
					Statement stmt = con.createStatement();
					ResultSet rs = stmt.executeQuery(sql.toString());
					while( rs.next() ) {
						Long nodeId = rs.getLong("node_id");
						Long lastNodeId = rs.getLong("last_node_id");
						visitedNodeCache.put(nodeId,lastNodeId);
						notCached.remove(nodeId);
					}
					rs.close();
					stmt.close();
					con.close();
				} catch(SQLException e) {
					throw new RuntimeException(e);
				}
				for( Long nodeId : notCached ) {
					visitedNodeCache.put(nodeId,null);
				}
			}
			Map<Long,Long> map = new HashMap<Long,Long>();
			for( Long nodeId : nodeIds ) {
				map.put( nodeId, visitedNodeCache.get(nodeId) );
			}
			return map;
		}
	}

	public void markVisited(Node topic, long lastNodeId) {
		NodeImpl topicNode = (NodeImpl)topic;
		long nodeId = topicNode.getId();
		boolean updated = false;
		try {
			Connection con = db().getConnection();
			try {
				Long persistedLastVisitedNodeId = lastVisitedNodeId(nodeId);
				if( persistedLastVisitedNodeId == null ) {
					PreparedStatement stmt = con.prepareStatement(
						"insert into visited (user_id, node_id, last_node_id)"
						+" values (?, ?, ?)"
					);
					stmt.setLong( 1, getId() );
					stmt.setLong( 2, nodeId );
					stmt.setLong( 3, lastNodeId );
					DbDatabaseImpl.executeUpdateIgnoringDuplicateKeys(stmt);
					stmt.close();
					updated = true;
				} else if (lastNodeId > persistedLastVisitedNodeId) {
					PreparedStatement stmt = con.prepareStatement(
						"update visited set last_node_id = ?"
						+" where user_id = ? and node_id = ?"
					);
					stmt.setLong( 1, lastNodeId );
					stmt.setLong( 2, getId() );
					stmt.setLong( 3, nodeId );
					stmt.executeUpdate();
					stmt.close();
					updated = true;
				}
			} finally {
				con.close();
			}
		} catch(SQLException e) {
			if( !e.getMessage().contains("violates foreign key constraint \"visited_last_node_id_fkey\"") )
				throw new RuntimeException(e);
		}
		if (updated) {
			synchronized(visitedNodeCache) {
				visitedNodeCache.remove(nodeId);
			}
		}
	}

	public void unmarkVisited(Node node) {
		long nodeId = node.getId();
		try {
			Connection con = db().getConnection();
			PreparedStatement stmt = con.prepareStatement(
				"delete from visited"
				+" where user_id = ? and node_id = ?"
			);
			stmt.setLong( 1, getId() );
			stmt.setLong( 2, nodeId );
			stmt.executeUpdate();
			stmt.close();
			con.close();
		} catch(SQLException e) {
			throw new RuntimeException(e);
		}
		synchronized(visitedNodeCache) {
			visitedNodeCache.remove(nodeId);
		}
	}


	static void addPostInsertListener(final Listener<? super UserImpl> listener) {
		postInsertListeners.add(listener);
	}



	private final Memoizer<String,String> propertyCache = new Memoizer<String,String>(new Computable<String,String>() {
		public String get(String key) {
			try {
				Connection con = db().getConnection();
				PreparedStatement stmt = con.prepareStatement(
					"select value from user_property where user_id = ? and key = ?"
				);
				stmt.setLong( 1, getId() );
				stmt.setString( 2, key );
				ResultSet rs = stmt.executeQuery();
				try {
					return rs.next() ? rs.getString("value") : null;
				} finally {
					rs.close();
					stmt.close();
					con.close();
				}
			} catch(SQLException e) {
				throw new RuntimeException(e);
			}
		}
	});

	public String getProperty(String key) {
		return propertyCache.get(key);
	}

	public void setProperty(String key,String value) {
		try {
			Connection con = db().getConnection();
			PreparedStatement stmt = con.prepareStatement(
				"delete from user_property where user_id = ? and key = ?"
			);
			stmt.setLong( 1, getId() );
			stmt.setString( 2, key );
			stmt.executeUpdate();
			stmt.close();
			if( value != null ) {
				stmt = con.prepareStatement(
					"insert into user_property (user_id,key,value) values (?,?,?)"
				);
				stmt.setLong( 1, getId() );
				stmt.setString( 2, key );
				stmt.setString( 3, value );
				stmt.executeUpdate();
				stmt.close();
			}
			con.close();
		} catch(SQLException e) {
			throw new RuntimeException(e);
		} finally {
			propertyCache.remove(key);
		}
	}


	final Memoizer<String,Boolean> tagCache = new Memoizer<String,Boolean>(new Computable<String,Boolean>() {
		public Boolean get(String sqlCondition) {
			return TagImpl.countTags(siteKey,sqlCondition) > 0;
		}
	});





	private final static PooledStringDigester passwordDigester = new PooledStringDigester();

	static {
        passwordDigester.setAlgorithm(Init.get("passwordDigestAlgorithm","SHA-256"));
        passwordDigester.setIterations(Init.get("passwordDigestIterations",100000));
        passwordDigester.setSaltSizeBytes(Init.get("passwordDigestSaltSize",16));
        passwordDigester.setPoolSize(Init.get("passwordDigestPoolSize",4));
        passwordDigester.initialize();
	}

	private final static PooledStringDigester passcookieDigester = new PooledStringDigester();

	static {
        passcookieDigester.setAlgorithm(Init.get("passcookieDigestAlgorithm","SHA-256"));
        passcookieDigester.setIterations(Init.get("passcookieDigestIterations",100000));
        FixedByteArraySaltGenerator sg = new FixedByteArraySaltGenerator();
        // this fixed salt needs to be kept secret
        sg.setSalt(Init.get("passcookieSalt", new byte[]{105, 4, 40, 78, 24, 46, 30, 100, 18, -27, 114, -21, -44, -59, 103, 43}));
        passcookieDigester.setSaltGenerator(sg);
        passcookieDigester.setPoolSize(Init.get("passcookieDigestPoolSize",4));
        passcookieDigester.initialize();
	}

	private final static PooledStringDigester resetcodeDigester = new PooledStringDigester();

	static {
        resetcodeDigester.setAlgorithm(Init.get("resetcodeDigestAlgorithm","SHA-256"));
        resetcodeDigester.setIterations(Init.get("resetcodeDigestIterations",100000));
        FixedByteArraySaltGenerator sg = new FixedByteArraySaltGenerator();
        // this fixed salt needs to be kept secret
        sg.setSalt(Init.get("resetcodeSalt", new byte[]{-47, 9, -128, 109, 112, -88, -91, 39, 77, 111, 57, -102, 120, 12, 54, 16}));
        resetcodeDigester.setSaltGenerator(sg);
        resetcodeDigester.setPoolSize(Init.get("resetcodeDigestPoolSize",4));
        resetcodeDigester.initialize();
	}

	public boolean checkPassword(String password) {
		return passwordDigest!=null && passwordDigester.matches(password, passwordDigest);
	}

	private String calcPasscookie() {
		return passcookieDigester.digest(passwordDigest);
	}

	public boolean checkPasscookie(String passcookie) {
		return passwordDigest!=null && getPasscookie().equals(passcookie);
	}

	public String getResetcode() {
		return resetcodeDigester.digest(passwordDigest);
	}

	public boolean checkResetcode(String resetcode) {
		return passwordDigest!=null && getResetcode().equals(resetcode);
	}

	private static String digestPassword(String password) {
		return passwordDigester.digest(password);
	}

}