comparison src/nabble/model/UserImpl.java @ 0:7ecd1a4ef557

add content
author Franklin Schmidt <fschmidt@gmail.com>
date Thu, 21 Mar 2019 19:15:52 -0600
parents
children cc5b7d515580
comparison
equal deleted inserted replaced
-1:000000000000 0:7ecd1a4ef557
1 /*
2
3 Copyright (C) 2003 Franklin Schmidt <frank@gustos.com>
4
5 */
6
7 package nabble.model;
8
9 import fschmidt.db.DbDatabase;
10 import fschmidt.db.DbNull;
11 import fschmidt.db.DbObjectFactory;
12 import fschmidt.db.DbRecord;
13 import fschmidt.db.DbTable;
14 import fschmidt.db.DbUtils;
15 import fschmidt.db.Listener;
16 import fschmidt.db.ListenerList;
17 import fschmidt.db.LongKey;
18 import fschmidt.db.postgres.DbDatabaseImpl;
19 import fschmidt.util.java.Computable;
20 import fschmidt.util.java.Memoizer;
21 import fschmidt.util.java.ObjectUtils;
22 import fschmidt.util.java.SimpleCache;
23 import fschmidt.util.java.TimedCacheMap;
24 import fschmidt.util.mail.MailAddress;
25 import org.jasypt.digest.PooledStringDigester;
26 import org.jasypt.salt.FixedByteArraySaltGenerator;
27 import org.slf4j.Logger;
28 import org.slf4j.LoggerFactory;
29
30 import java.awt.image.BufferedImage;
31 import java.sql.Connection;
32 import java.sql.PreparedStatement;
33 import java.sql.ResultSet;
34 import java.sql.SQLException;
35 import java.sql.Statement;
36 import java.util.ArrayList;
37 import java.util.Collection;
38 import java.util.Collections;
39 import java.util.Date;
40 import java.util.HashMap;
41 import java.util.HashSet;
42 import java.util.Iterator;
43 import java.util.List;
44 import java.util.Map;
45 import java.util.Set;
46 import java.util.WeakHashMap;
47 import java.util.concurrent.CopyOnWriteArrayList;
48
49
50 final class UserImpl extends PersonImpl implements User {
51 private static final Logger logger = LoggerFactory.getLogger(UserImpl.class);
52
53 final SiteKey siteKey;
54 private final DbRecord<LongKey,UserImpl> record;
55 private String email;
56 private String passwordDigest;
57 private String name;
58 private Date registered;
59 private boolean noArchive;
60 private Message signature = null;
61 private int bounces;
62
63 private UserImpl(SiteKey siteKey,LongKey key,ResultSet rs)
64 throws SQLException
65 {
66 this.siteKey = siteKey;
67 record = table(siteKey).newRecord(this,key);
68 email = rs.getString("email");
69 passwordDigest = rs.getString("password_digest");
70 name = rs.getString("name");
71 registered = DbUtils.getDate(rs,"registered");
72 noArchive = rs.getBoolean("no_archive");
73 String signatureRaw = rs.getString("signature");
74 String signatureFormatS = rs.getString("signature_format");
75 if( signatureRaw!=null && signatureFormatS!=null ) {
76 Message.Format signatureFormat = Message.Format.getMessageFormat( signatureFormatS.charAt(0) );
77 signature = new Message(signatureRaw,signatureFormat);
78 }
79 bounces = rs.getInt("bounces");
80 for( ExtensionFactory<User,?> factory : extensionFactories ) {
81 Object obj = factory.construct(this,rs);
82 if( obj != null )
83 getExtensionMap().put(factory,obj);
84 }
85 }
86
87 private UserImpl(SiteImpl site) {
88 this.siteKey = site.siteKey;
89 record = table(siteKey).newRecord(this);
90 }
91
92
93 public DbRecord<LongKey,UserImpl> getDbRecord() {
94 return record;
95 }
96
97 private DbTable<LongKey,UserImpl> table() {
98 return record.getDbTable();
99 }
100
101 private DbDatabase db() {
102 return table().getDbDatabase();
103 }
104
105 public long getId() {
106 return record.getPrimaryKey().value();
107 }
108
109 SiteImpl getSiteImpl() {
110 return siteKey.site();
111 }
112
113 public Site getSite() {
114 return getSiteImpl();
115 }
116
117 public boolean isDeactivated() {
118 return !isRegistered() && isNoArchive();
119 }
120
121 boolean isNoArchive() {
122 return noArchive;
123 }
124
125 public void setNoArchive(boolean noArchive) {
126 if( this.noArchive == noArchive )
127 return;
128
129 if( !db().isInTransaction() ) {
130 db().beginTransaction();
131 try {
132 UserImpl user = DbUtils.getGoodCopy(this);
133 user.setNoArchive(noArchive);
134 user.getDbRecord().update();
135 db().commitTransaction();
136 return;
137 } finally {
138 db().endTransaction();
139 }
140 }
141 this.noArchive = noArchive;
142 record.fields().put("no_archive",DbNull.fix(noArchive));
143 }
144
145 public String getEmail() {
146 return email;
147 }
148
149 static void validateEmail(String email) throws ModelException.EmailFormat {
150 if (!new MailAddress(email).isValid()) {
151 throw new ModelException.EmailFormat(email);
152 }
153 }
154
155 public void setEmail(String email) throws ModelException {
156 if( !db().isInTransaction() ) {
157 db().beginTransaction();
158 try {
159 UserImpl user = DbUtils.getGoodCopy(this);
160 user.setEmail(email);
161 user.getDbRecord().update();
162 db().commitTransaction();
163 return;
164 } finally {
165 db().endTransaction();
166 }
167 }
168 validateEmail(email);
169 setEmail2(email);
170 }
171
172 private void setEmail2(String email) throws ModelException {
173 if( email.equals(this.email) )
174 return;
175 SiteImpl site = getSiteImpl();
176 if( site.getUserImplFromEmail(email) != null )
177 throw ModelException.newInstance("email_already_in_user","Email already in use");
178 this.email = email;
179 record.fields().put("email",email);
180 }
181
182 public String getPasswordDigest() {
183 return passwordDigest;
184 }
185
186 public void setPassword(String password) throws ModelException {
187 if( "".equals(password) )
188 throw ModelException.newInstance("empty_password","Password cannot be empty");
189 setPasswordDigest(digestPassword(password));
190 }
191
192 public void setPasswordDigest(String passwordDigest) {
193 if( ObjectUtils.equals(passwordDigest,this.passwordDigest) )
194 return;
195 this.passwordDigest = passwordDigest;
196 record.fields().put("password_digest",DbNull.fix(passwordDigest));
197 synchronized (passcookieLock) {
198 this.passcookie = null;
199 }
200 }
201
202 private volatile String passcookie = null;
203 private Object passcookieLock = new Object();
204
205 public String getPasscookie() {
206 String p = passcookie;
207 if (p==null) {
208 synchronized (passcookieLock) {
209 p = passcookie;
210 if (p==null) {
211 p = calcPasscookie();
212 passcookie = p;
213 }
214 }
215 }
216 return p;
217 }
218
219 public String getName() {
220 return name;
221 }
222
223 public void setName(String name) throws ModelException {
224 setName(name,true);
225 }
226
227 private void setName(String name,boolean replaceUnregistered) throws ModelException {
228 name = name.trim();
229 if( name.equals("") )
230 throw ModelException.newInstance("empty_user_name","User name cannot be empty.");
231 if( name.equals(this.name) )
232 return;
233 if( !name.equalsIgnoreCase(this.name) ) {
234 UserImpl user = getSiteImpl().getUserImplFromName(name);
235 if( user != null ) {
236 if( !replaceUnregistered || user.isRegistered() )
237 throw ModelException.newInstance("user_name_already_in_use","User name '"+name+"' already in use");
238 user.setNameLike2(name);
239 user.update();
240 }
241 try {
242 Connection con = db().getConnection();
243 PreparedStatement stmt = con.prepareStatement(
244 "select 'x' from registration where email!=? and name=?"
245 );
246 stmt.setString(1,this.email);
247 stmt.setString(2,name);
248 try {
249 if( stmt.executeQuery().next() )
250 throw ModelException.newInstance("user_name_already_in_use","User name '"+name+"' already in use");
251 } finally {
252 stmt.close();
253 con.close();
254 }
255 } catch(SQLException e) {
256 throw new RuntimeException(e);
257 }
258 }
259 this.name = name;
260 record.fields().put("name",name);
261 }
262
263 void setNameLike(String name,boolean replaceUnregistered) {
264 try {
265 setName(name,replaceUnregistered);
266 } catch(ModelException e) {
267 setNameLike2(name);
268 }
269 }
270
271 private void setNameLike2(String name) {
272 for( int i=2; true; i++ ) {
273 try {
274 setName(name+"-"+i,false);
275 break;
276 } catch(ModelException e2) {}
277 }
278 }
279
280 /* To be called from the shell */
281 public void changeNameTo(String newName) {
282 db().beginTransaction();
283 try {
284 UserImpl u = (UserImpl) getGoodCopy();
285 u.setName(newName);
286 u.update();
287 db().commitTransaction();
288 DbUtils.uncache(u);
289 } catch (ModelException e) {
290 throw new RuntimeException(e);
291 } finally {
292 db().endTransaction();
293 }
294 }
295
296 public Date getRegistered() {
297 return registered;
298 }
299
300 void setRegistered(Date registered) {
301 if( ObjectUtils.equals(registered,this.registered) )
302 return;
303 this.registered = registered;
304 record.fields().put("registered",DbNull.fix(registered));
305 }
306
307 public boolean equals(Object obj) {
308 return obj instanceof User && ((User)obj).getId()==getId();
309 }
310
311 public int hashCode() {
312 return (int)getId();
313 }
314
315 public String toString() {
316 return record.isInDb() ? "user-"+getId() : "user-new";
317 }
318
319 public void register() throws ModelException {
320 register(new Date());
321 }
322
323 public void register(Date registerDate) throws ModelException {
324 if( !db().isInTransaction() ) {
325 db().beginTransaction();
326 try {
327 UserImpl user;
328 if( record.isInDb() ) {
329 user = DbUtils.getGoodCopy(this);
330 user.setEmail(email);
331 user.setName(name);
332 user.setPasswordDigest(passwordDigest);
333 } else {
334 user = this;
335 }
336 user.register();
337 db().commitTransaction();
338 } finally {
339 db().endTransaction();
340 }
341 return;
342 }
343 if( passwordDigest==null )
344 throw new RuntimeException();
345 setRegistered( registerDate );
346 if( record.isInDb() ) {
347 if( isNoArchive() )
348 setNoArchive(false);
349 record.update();
350 } else {
351 insert();
352 }
353 }
354
355 public boolean isRegistered() {
356 return record.isInDb() && registered!=null;
357 }
358
359 void insert() {
360 if( email==null || name==null )
361 throw new RuntimeException();
362 record.insert();
363 }
364
365 public void update() {
366 if( !db().isInTransaction() )
367 throw new RuntimeException("this should be done in a transaction");
368 Set<String> keys = record.fields().keySet();
369 if( keys.contains("name") || keys.contains("signature") ) {
370 getSiteImpl().update(); // fire change listeners
371 }
372 getDbRecord().update();
373 }
374
375 public User getGoodCopy() {
376 return DbUtils.getGoodCopy(this);
377 }
378
379
380
381
382 public int getExternalHash(String url) {
383 return (url.toLowerCase() + getId()).hashCode();
384 }
385
386
387 static final ListenerList<UserImpl> preUpdateListeners = new ListenerList<UserImpl>();
388 static final ListenerList<UserImpl> postInsertListeners = new ListenerList<UserImpl>();
389
390 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>>() {
391 public DbTable<LongKey,UserImpl> get(SiteKey siteKey) {
392 DbDatabase db = siteKey.getDb();
393 final long siteId = siteKey.getId();
394 DbTable<LongKey,UserImpl> table = db.newTable("user_",db.newIdentityLongKeySetter("user_id")
395 , new DbObjectFactory<LongKey,UserImpl>() {
396 public UserImpl makeDbObject(LongKey key,ResultSet rs,String tableName)
397 throws SQLException
398 {
399 SiteKey siteKey = SiteKey.getInstance(siteId);
400 return new UserImpl(siteKey,key,rs);
401 }
402 }
403 );
404 table.getPreUpdateListeners().add(preUpdateListeners);
405 table.getPostInsertListeners().add(postInsertListeners);
406 return table;
407 }
408 });
409
410 private static DbTable<LongKey,UserImpl> table(SiteKey siteKey) {
411 return tables.get(siteKey);
412 }
413
414 static UserImpl getUser(SiteKey siteKey,long id) {
415 UserImpl user = table(siteKey).findByPrimaryKey(new LongKey(id));
416 if( user==null )
417 logger.warn("user "+id+" not found");
418 return user;
419 }
420
421 static Collection<UserImpl> getUsers(SiteKey siteKey,Collection<Long> ids) {
422 List<LongKey> list = new ArrayList<LongKey>();
423 for( long id : ids ) {
424 list.add( new LongKey(id) );
425 }
426 return table(siteKey).findByPrimaryKey(list).values();
427 }
428
429 static UserImpl getUser(SiteKey siteKey,ResultSet rs)
430 throws SQLException
431 {
432 return table(siteKey).getDbObject(rs);
433 }
434
435 static void getUsers(SiteKey siteKey,PreparedStatement stmt,List<? super UserImpl> list)
436 throws SQLException
437 {
438 ResultSet rs = stmt.executeQuery();
439 while( rs.next() ) {
440 UserImpl user = getUser(siteKey,rs);
441 list.add(user);
442 }
443 rs.close();
444 stmt.close();
445 }
446
447 static List<UserImpl> getUsers(SiteKey siteKey,PreparedStatement stmt)
448 throws SQLException
449 {
450 List<UserImpl> list = new ArrayList<UserImpl>();
451 getUsers(siteKey,stmt,list);
452 return list;
453 }
454
455 private static UserImpl getUser(SiteImpl site,String val,String sql) {
456 try {
457 SiteKey siteKey = site.siteKey;
458 Connection con = siteKey.getDb().getConnection();
459 PreparedStatement stmt = con.prepareStatement(sql);
460 stmt.setString(1,val);
461 ResultSet rs = stmt.executeQuery();
462 UserImpl user = rs.next() ? getUser(siteKey,rs) : null;
463 rs.close();
464 stmt.close();
465 con.close();
466 return user;
467 } catch(SQLException e) {
468 throw new RuntimeException(e);
469 }
470 }
471
472 static UserImpl getUserFromEmail(SiteImpl site,String email) {
473 return getUser( site, email.toLowerCase(),
474 "select * from user_"
475 +" where lower(email)=?"
476 );
477 }
478
479 static UserImpl getUserFromName(SiteImpl site,String name) {
480 return getUser( site, name.toLowerCase(),
481 "select * from user_"
482 +" where lower(name)=?"
483 );
484 }
485
486 static UserImpl createGhost(SiteImpl site,String email) {
487 UserImpl user = new UserImpl(site);
488 try {
489 user.setEmail2(email);
490 } catch(ModelException e) {
491 throw new RuntimeException(e);
492 }
493 return user;
494 }
495
496 // Subscriptions -----------------------------------------------------------
497
498 public boolean isSubscribed(Node node) {
499 return SubscriptionImpl.isSubscribed(this, (NodeImpl) node);
500 }
501
502 public Subscription getSubscription(Node node) {
503 return SubscriptionImpl.getSubscription( this, (NodeImpl)node );
504 }
505
506 public Subscription subscribe(Node node,Subscription.To to,Subscription.Type type) {
507 clearBounces();
508 Subscription subscription = getSubscription(node);
509 if( subscription != null ) {
510 subscription.setTo(to);
511 subscription.setType(type);
512 return subscription;
513 } else {
514 return SubscriptionImpl.insert( this, (NodeImpl)node, to, type );
515 }
516 }
517
518 /*10 posts in 5 minutes */
519 private static final RecentPostLimit postLimit1 = new RecentPostLimit(5 * 60 * 1000L, 10);
520
521 /* 30 posts in 15 minutes */
522 private static final RecentPostLimit postLimit2 = new RecentPostLimit(15 * 60 * 1000L, 30);
523
524 void updateNewPostLimit() {
525 String key = siteKey.getId() + "-" + record.getPrimaryKey().value();
526 postLimit1.insert(key);
527 postLimit2.insert(key);
528 }
529
530 public boolean hasTooManyPosts() {
531 String key = siteKey.getId() + "-" + record.getPrimaryKey().value();
532 return postLimit1.hasTooManyPosts(key) || postLimit2.hasTooManyPosts(key);
533 }
534
535 private static class RecentPostLimit {
536 private final long timeLimit;
537 private final int postLimit;
538 private final Map<String,long[]> floodMap;
539
540 private RecentPostLimit(long timeLimit, int postLimit) {
541 this.timeLimit = timeLimit;
542 this.postLimit = postLimit;
543 this.floodMap = new TimedCacheMap<String,long[]>(timeLimit);
544 }
545
546 public void insert(String key) {
547 long[] recentPostTimes;
548 synchronized(floodMap) {
549 recentPostTimes = floodMap.get(key);
550 if( recentPostTimes==null ) {
551 recentPostTimes = new long[postLimit];
552 floodMap.put(key,recentPostTimes);
553 }
554 }
555 long now = System.currentTimeMillis();
556 long recently = now - timeLimit;
557 synchronized(recentPostTimes) {
558 for( int i=0; i<recentPostTimes.length; i++ ) {
559 if( recentPostTimes[i] < recently ) {
560 recentPostTimes[i] = now;
561 return;
562 }
563 }
564 }
565 }
566
567 public boolean hasTooManyPosts(String key) {
568 long[] recentPostTimes;
569 synchronized(floodMap) {
570 recentPostTimes = floodMap.get(key);
571 if (recentPostTimes==null)
572 return false;
573 }
574 long now = System.currentTimeMillis();
575 long recently = now - timeLimit;
576 synchronized(recentPostTimes) {
577 for (long time : recentPostTimes) {
578 if (time < recently) {
579 return false;
580 }
581 }
582 }
583 return true;
584 }
585 }
586
587
588 static UserImpl getOrCreateUnregisteredUser(SiteImpl site,String email,String name)
589 throws ModelException
590 {
591 DbDatabase db = site.getDb();
592 if( !db.isInTransaction() ) {
593 db.beginTransaction();
594 try {
595 UserImpl user = getOrCreateUnregisteredUser(site,email,name);
596 db.commitTransaction();
597 return user;
598 } finally {
599 db.endTransaction();
600 }
601 }
602 UserImpl user = site.getUserImplFromEmail(email);
603 if( user==null ) {
604 user = new UserImpl(site);
605 user.setEmail(email);
606 } else {
607 if( user.isRegistered() )
608 throw ModelException.newInstance("email_already_registered","This email is already registered");
609 validateEmail(user.getEmail());
610 }
611 user.setName(name);
612 if( !user.record.isInDb() ) {
613 user.insert();
614 } else if( !user.record.fields().isEmpty() ) {
615 user.update();
616 }
617 return user;
618 }
619
620 // registration
621
622 static UserImpl createUser(SiteImpl site,String email,String password,String name) throws ModelException {
623 return createUser2(site, email, digestPassword(password), name);
624 }
625
626 private static UserImpl createUser2(SiteImpl site,String email,String passwordDigest,String name) throws ModelException {
627 // transaction used because setName() may update user
628 DbDatabase db = site.getDb();
629 if( !db.isInTransaction() ) {
630 db.beginTransaction();
631 try {
632 UserImpl user = createUser2(site,email,passwordDigest,name);
633 db.commitTransaction();
634 return user;
635 } finally {
636 db.endTransaction();
637 }
638 }
639 if (!new MailAddress(email).isValid()) {
640 throw new ModelException.EmailFormat("invalid_email");
641 }
642 UserImpl user = site.getUserImplFromEmail(email);
643 if( user==null ) {
644 user = new UserImpl(site);
645 user.setEmail(email);
646 } else {
647 if( user.isRegistered() )
648 throw ModelException.newInstance("user_already_registered","User is already registered");
649 validateEmail(user.getEmail());
650 }
651 user.setPasswordDigest(passwordDigest);
652 user.setName(name);
653 return user;
654 }
655
656 static UserImpl getOrCreateUser(SiteImpl site,String email) {
657 UserImpl user = site.getUserImplFromEmail(email);
658 if (user == null) {
659 String username = email.substring(0, email.indexOf('@'));
660 user = createGhost(site,email);
661 user.setNameLike(username, false);
662 user.insert();
663 }
664 return user;
665 }
666
667 private static final Object regLock = new Object();
668
669 String newRegistration(String nextUrl) {
670 if( nextUrl.equals("null") )
671 throw new RuntimeException("nextUrl is \"null\"");
672 synchronized(regLock) {
673 String key;
674 try {
675 Connection con = db().getConnection();
676 {
677 PreparedStatement stmt = con.prepareStatement(
678 "select 'x' from registration where key_=?"
679 );
680 do {
681 key = Double.toString(Math.random());
682 stmt.setString(1,key);
683 } while( stmt.executeQuery().next() );
684 stmt.close();
685 }
686 {
687 PreparedStatement stmt = con.prepareStatement(
688 "insert into registration"
689 +" ( key_, email, password_digest, name, next_url ) values (?,?,?,?,?)"
690 );
691 int i = 0;
692 stmt.setString(++i,key);
693 stmt.setString(++i,getEmail());
694 stmt.setString(++i,getPasswordDigest());
695 stmt.setString(++i,getName());
696 stmt.setString(++i,nextUrl);
697 stmt.executeUpdate();
698 stmt.close();
699 }
700 {
701 Statement stmt = con.createStatement();
702 stmt.executeUpdate(
703 "delete from registration where date_<" + Db.arcana.dateSub("now()",7,"day")
704 );
705 stmt.close();
706 }
707 con.close();
708 } catch(SQLException e) {
709 throw new RuntimeException(e);
710 }
711 return key;
712 }
713 }
714
715 static User getRegistration(SiteImpl site,String registrationKey)
716 throws ModelException
717 {
718 try {
719 DbDatabase db = site.getDb();
720 Connection con = db.getConnection();
721 PreparedStatement stmt = con.prepareStatement(
722 "select * from registration where key_=?"
723 );
724 stmt.setString(1,registrationKey);
725 ResultSet rs = stmt.executeQuery();
726 try {
727 if( !rs.next() )
728 return null;
729 String email = rs.getString("email");
730 String passwordDigest = rs.getString("password_digest");
731 String name = rs.getString("name");
732 return createUser2(site,email,passwordDigest,name);
733 } finally {
734 rs.close();
735 stmt.close();
736 con.close();
737 }
738 } catch(SQLException e) {
739 throw new RuntimeException(e);
740 }
741 }
742
743 static String getNextUrl(SiteKey siteKey,String registrationKey)
744 {
745 try {
746 Connection con = siteKey.getDb().getConnection();
747 PreparedStatement stmt = con.prepareStatement(
748 "select next_url from registration where key_=?"
749 );
750 stmt.setString(1,registrationKey);
751 ResultSet rs = stmt.executeQuery();
752 try {
753 if( !rs.next() )
754 return null;
755 return rs.getString("next_url");
756 } finally {
757 rs.close();
758 stmt.close();
759 con.close();
760 }
761 } catch(SQLException e) {
762 throw new RuntimeException(e);
763 }
764 }
765
766 // Called from beanshell
767 private static void deletePendingRegistration(Site site,String email, String username) {
768 try {
769 Connection con = site.getDb().getConnection();
770 PreparedStatement stmt = con.prepareStatement(
771 "delete from registration where email=? or name = ?"
772 );
773 stmt.setString(1,email);
774 stmt.setString(2,username);
775 stmt.executeUpdate();
776 stmt.close();
777 con.close();
778 } catch(SQLException e) {
779 throw new RuntimeException(e);
780 }
781 }
782
783 public void deactivate() {
784 db().beginTransaction();
785 try {
786 UserImpl user = DbUtils.getGoodCopy(this);
787 user.setNoArchive(true);
788 user.setRegistered(null);
789 user.setPasswordDigest(null);
790 user.record.update();
791 db().commitTransaction();
792 logger.info("User removed his/her account: " + getEmail());
793 } finally {
794 db().endTransaction();
795 }
796 }
797
798 private DbParamSetter simpleParamSetter() {
799 return new DbParamSetter() {
800 public void setParams(PreparedStatement stmt) throws SQLException {
801 stmt.setLong( 1, getId() );
802 }
803 };
804 }
805
806 public NodeIterator<? extends Node> getPendingPosts() {
807 return new CursorNodeIterator( siteKey,
808 "select *"
809 +" from node"
810 +" where owner_id = ?"
811 +" and when_sent is not null"
812 +" order by when_sent"
813 , simpleParamSetter()
814 );
815 }
816
817 public Message getSignature() {
818 return signature;
819 }
820
821 public User setSignature( String signatureRaw, Message.Format signatureFormat )
822 throws ModelException
823 {
824 if( !db().isInTransaction() ) {
825 db().beginTransaction();
826 try {
827 UserImpl user = DbUtils.getGoodCopy(this);
828 user.setSignature(signatureRaw,signatureFormat);
829 user.getDbRecord().update();
830 db().commitTransaction();
831 return DbUtils.getGoodCopy(user);
832 } finally {
833 db().endTransaction();
834 }
835 }
836 if( signatureRaw==null || signatureRaw.trim().length()==0 ) {
837 if( signature != null ) {
838 signature = null;
839 record.fields().put("signature",DbNull.STRING);
840 record.fields().put("signature_format",DbNull.STRING);
841 }
842 } else {
843 Message newSignature = new Message(signatureRaw,signatureFormat);
844 if( !newSignature.equals(signature) ) {
845 signature = newSignature;
846 record.fields().put("signature",signatureRaw);
847 record.fields().put("signature_format",Character.toString(signatureFormat.getCode()));
848 }
849 }
850 return this;
851 }
852
853
854 public String getDecoratedAddress(Node node) {
855 return PostByEmail.getMailAddress(this, node);
856 }
857
858 public void saveAvatar(BufferedImage smallImage,BufferedImage bigImage) throws ModelException {
859 if( !db().isInTransaction() ) {
860 db().beginTransaction();
861 try {
862 DbUtils.getGoodCopy(this).saveAvatar(smallImage,bigImage);
863 db().commitTransaction();
864 } finally {
865 db().endTransaction();
866 }
867 return;
868 }
869 Message.AvatarSource as = new Message.AvatarSource(this);
870 FileUpload.saveImage(smallImage,ModelHome.AVATAR_SMALL,as);
871 FileUpload.saveImage(bigImage,ModelHome.AVATAR_BIG,as);
872 getSiteImpl().update(); // fire change listeners
873 DbUtils.uncache(this);
874 }
875
876 public void deleteAvatar() {
877 if( !db().isInTransaction() ) {
878 db().beginTransaction();
879 try {
880 DbUtils.getGoodCopy(this).deleteAvatar();
881 db().commitTransaction();
882 } finally {
883 db().endTransaction();
884 }
885 return;
886 }
887 final Message.AvatarSource as = new Message.AvatarSource(this);
888 FileUpload.deleteFile(ModelHome.AVATAR_SMALL,as);
889 FileUpload.deleteFile(ModelHome.AVATAR_BIG,as);
890 getSiteImpl().update(); // fire change listeners
891 db().runAfterCommit(new Runnable(){public void run(){
892 FileUpload.fireFileUpdateListeners(as);
893 }});
894 DbUtils.uncache(this);
895 }
896
897 private boolean hasAvatar;
898 private boolean checkedAvatar = false;
899
900 public synchronized boolean hasAvatar() {
901 if( !checkedAvatar ) {
902 Message.AvatarSource as = new Message.AvatarSource(this);
903 hasAvatar = FileUpload.hasFile(as,ModelHome.AVATAR_SMALL) && FileUpload.hasFile(as,ModelHome.AVATAR_BIG);
904 checkedAvatar = true;
905 }
906 return hasAvatar;
907 }
908
909
910
911 public Node newRootNode(Node.Kind kind,String subject,String message,Message.Format msgFmt,Site site,String type) throws ModelException {
912 return NodeImpl.newRootNode(kind,this,subject,message,msgFmt,(SiteImpl)site,type);
913 }
914
915 public Node newChildNode(Node.Kind kind,String subject,String message,Message.Format msgFmt,Node parent) throws ModelException {
916 return NodeImpl.newChildNode(kind,this,subject,message,msgFmt,(NodeImpl)parent);
917 }
918
919 public String getSearchId() {
920 return Long.toString(getId());
921 }
922
923 public String getIdString() {
924 return Long.toString(getId());
925 }
926
927 private void clearBounces() {
928 if( bounces==0 )
929 return;
930 bounces = 0;
931 record.fields().put("bounces",DbNull.INTEGER);
932 record.update();
933 }
934
935 void bounced() {
936 record.fields().put("bounces",++bounces);
937 record.update();
938 }
939
940 int getBounces() {
941 return bounces;
942 }
943
944 private static final int bounceLimit = Init.get("bounceLimit",100);
945
946 boolean isAutoUnsubscribe() {
947 return isDeactivated() || bounces > bounceLimit;
948 }
949
950
951
952
953 private volatile Map<String, Integer> nodeCount = new HashMap<String, Integer>();
954
955 public final int getNodeCount(String cnd) {
956 String key = cnd == null? "none" : cnd;
957 if (!nodeCount.containsKey(key)) {
958 try {
959 Connection con = db().getConnection();
960 PreparedStatement stmt = con.prepareStatement(
961 "select count(*) as n from node where owner_id = ?" +
962 (cnd == null? "" : " and " + cnd)
963 );
964 stmt.setLong(1,getId());
965 ResultSet rs = stmt.executeQuery();
966 rs.next();
967 nodeCount.put(key, rs.getInt("n"));
968 rs.close();
969 stmt.close();
970 con.close();
971 } catch(SQLException e) {
972 throw new RuntimeException(e);
973 }
974 }
975 return nodeCount.get(key);
976 }
977
978 void setNodeCount(int nodeCount) {
979 this.nodeCount.put("none", nodeCount);
980 }
981
982 static {
983 Listener<NodeImpl> listener = new Listener<NodeImpl>() {
984 public void event(NodeImpl node) {
985 table(node.siteKey).uncache(new LongKey(node.getOwnerId()));
986 }
987 };
988 NodeImpl.postInsertListeners.add(listener);
989 NodeImpl.postDeleteListeners.add(listener);
990 }
991
992
993 public void moveToRegisteredAccount(final String cookie) {
994 List<NodeImpl> nodes = new CursorNodeIterator( siteKey,
995 "select * from node where cookie=?"
996 ,
997 new DbParamSetter() {
998 public void setParams(PreparedStatement stmt) throws SQLException {
999 stmt.setString(1,cookie);
1000 }
1001 }
1002 ).asList();
1003 for( NodeImpl n : nodes ) {
1004 n.setOwner(this);
1005 n.update();
1006 }
1007 }
1008
1009
1010 public NodeIterator<? extends Node> getNodesByDateDesc(String cnd) {
1011 return new CursorNodeIterator( siteKey,
1012 "select * from node where owner_id = ?" +
1013 (cnd == null? "" : " and " + cnd) +
1014 " order by when_created desc"
1015 ,
1016 new DbParamSetter() {
1017 public void setParams(PreparedStatement stmt) throws SQLException {
1018 stmt.setLong( 1, getId() );
1019 }
1020 }
1021 );
1022 }
1023
1024
1025 public int deleteNodes() {
1026 List<NodeImpl> nodes = new CursorNodeIterator( siteKey,
1027 "select *"
1028 +" from node"
1029 +" where owner_id = ?"
1030 , simpleParamSetter()
1031 ).asList();
1032 int n = 0;
1033 for( NodeImpl node : nodes ) {
1034 db().beginTransaction();
1035 try {
1036 DbUtils.getGoodCopy(node).deleteMessageOrNode();
1037 db().commitTransaction();
1038 n++;
1039 } finally {
1040 db().endTransaction();
1041 }
1042 }
1043 return n;
1044 }
1045
1046 public int deleteNodesRecursively() {
1047 List<NodeImpl> nodes = new CursorNodeIterator( siteKey,
1048 "select *"
1049 +" from node"
1050 +" where owner_id = ?"
1051 , simpleParamSetter()
1052 ).asList();
1053 int n = 0;
1054 for( NodeImpl node : nodes ) {
1055 db().beginTransaction();
1056 try {
1057 DbUtils.getGoodCopy(node).deleteRecursively();
1058 db().commitTransaction();
1059 n++;
1060 } finally {
1061 db().endTransaction();
1062 }
1063 }
1064 return n;
1065 }
1066
1067
1068 private Map<ExtensionFactory<User,?>,Object> extensionMap;
1069
1070 private synchronized Map<ExtensionFactory<User, ?>, Object> getExtensionMap() {
1071 if (extensionMap == null)
1072 extensionMap = new HashMap<ExtensionFactory<User, ?>, Object>();
1073 return extensionMap;
1074 }
1075
1076 public <T> T getExtension(ExtensionFactory<User,T> factory) {
1077 synchronized(getExtensionMap()) {
1078 Object obj = extensionMap.get(factory);
1079 if( obj == null ) {
1080 obj = factory.construct(this);
1081 if( obj != null )
1082 extensionMap.put(factory,obj);
1083 }
1084 return factory.extensionClass().cast(obj);
1085 }
1086 }
1087
1088 private static Collection<ExtensionFactory<User,?>> extensionFactories = new CopyOnWriteArrayList<ExtensionFactory<User,?>>();
1089
1090 static <T> void addExtensionFactory(ExtensionFactory<User,T> factory) {
1091 extensionFactories.add(factory);
1092 Db.clearCache();
1093 }
1094
1095
1096
1097
1098 // visited node
1099
1100 private final Map<Long,Long> visitedNodeCache = new HashMap<Long,Long>();
1101
1102 public Long lastVisitedNodeId(long nodeId) {
1103 synchronized(visitedNodeCache) {
1104 return visitedNodeCache.containsKey(nodeId) ? visitedNodeCache.get(nodeId) : lastVisitedNodeIds(Collections.singletonList(nodeId)).get(nodeId);
1105 }
1106 }
1107
1108 public Map<Long,Long> lastVisitedNodeIds(Collection<Long> nodeIds) {
1109 synchronized(visitedNodeCache) {
1110 Set<Long> notCached = new HashSet<Long>();
1111 for( Long nodeId : nodeIds ) {
1112 if( !visitedNodeCache.containsKey(nodeId) )
1113 notCached.add(nodeId);
1114 }
1115 if( !notCached.isEmpty() ) {
1116 StringBuilder sql = new StringBuilder();
1117 sql
1118 .append( "select node_id, last_node_id from visited where user_id = " )
1119 .append( getId() )
1120 .append( " and node_id in (" )
1121 ;
1122 Iterator<Long> iter = notCached.iterator();
1123 sql.append( iter.next() );
1124 while( iter.hasNext() ) {
1125 sql.append( ',' ).append( iter.next() );
1126 }
1127 sql.append( ")" );
1128 try {
1129 Connection con = db().getConnection();
1130 Statement stmt = con.createStatement();
1131 ResultSet rs = stmt.executeQuery(sql.toString());
1132 while( rs.next() ) {
1133 Long nodeId = rs.getLong("node_id");
1134 Long lastNodeId = rs.getLong("last_node_id");
1135 visitedNodeCache.put(nodeId,lastNodeId);
1136 notCached.remove(nodeId);
1137 }
1138 rs.close();
1139 stmt.close();
1140 con.close();
1141 } catch(SQLException e) {
1142 throw new RuntimeException(e);
1143 }
1144 for( Long nodeId : notCached ) {
1145 visitedNodeCache.put(nodeId,null);
1146 }
1147 }
1148 Map<Long,Long> map = new HashMap<Long,Long>();
1149 for( Long nodeId : nodeIds ) {
1150 map.put( nodeId, visitedNodeCache.get(nodeId) );
1151 }
1152 return map;
1153 }
1154 }
1155
1156 public void markVisited(Node topic, long lastNodeId) {
1157 NodeImpl topicNode = (NodeImpl)topic;
1158 long nodeId = topicNode.getId();
1159 boolean updated = false;
1160 try {
1161 Connection con = db().getConnection();
1162 try {
1163 Long persistedLastVisitedNodeId = lastVisitedNodeId(nodeId);
1164 if( persistedLastVisitedNodeId == null ) {
1165 PreparedStatement stmt = con.prepareStatement(
1166 "insert into visited (user_id, node_id, last_node_id)"
1167 +" values (?, ?, ?)"
1168 );
1169 stmt.setLong( 1, getId() );
1170 stmt.setLong( 2, nodeId );
1171 stmt.setLong( 3, lastNodeId );
1172 DbDatabaseImpl.executeUpdateIgnoringDuplicateKeys(stmt);
1173 stmt.close();
1174 updated = true;
1175 } else if (lastNodeId > persistedLastVisitedNodeId) {
1176 PreparedStatement stmt = con.prepareStatement(
1177 "update visited set last_node_id = ?"
1178 +" where user_id = ? and node_id = ?"
1179 );
1180 stmt.setLong( 1, lastNodeId );
1181 stmt.setLong( 2, getId() );
1182 stmt.setLong( 3, nodeId );
1183 stmt.executeUpdate();
1184 stmt.close();
1185 updated = true;
1186 }
1187 } finally {
1188 con.close();
1189 }
1190 } catch(SQLException e) {
1191 if( !e.getMessage().contains("violates foreign key constraint \"visited_last_node_id_fkey\"") )
1192 throw new RuntimeException(e);
1193 }
1194 if (updated) {
1195 synchronized(visitedNodeCache) {
1196 visitedNodeCache.remove(nodeId);
1197 }
1198 }
1199 }
1200
1201 public void unmarkVisited(Node node) {
1202 long nodeId = node.getId();
1203 try {
1204 Connection con = db().getConnection();
1205 PreparedStatement stmt = con.prepareStatement(
1206 "delete from visited"
1207 +" where user_id = ? and node_id = ?"
1208 );
1209 stmt.setLong( 1, getId() );
1210 stmt.setLong( 2, nodeId );
1211 stmt.executeUpdate();
1212 stmt.close();
1213 con.close();
1214 } catch(SQLException e) {
1215 throw new RuntimeException(e);
1216 }
1217 synchronized(visitedNodeCache) {
1218 visitedNodeCache.remove(nodeId);
1219 }
1220 }
1221
1222
1223 static void addPostInsertListener(final Listener<? super UserImpl> listener) {
1224 postInsertListeners.add(listener);
1225 }
1226
1227
1228
1229 private final Memoizer<String,String> propertyCache = new Memoizer<String,String>(new Computable<String,String>() {
1230 public String get(String key) {
1231 try {
1232 Connection con = db().getConnection();
1233 PreparedStatement stmt = con.prepareStatement(
1234 "select value from user_property where user_id = ? and key = ?"
1235 );
1236 stmt.setLong( 1, getId() );
1237 stmt.setString( 2, key );
1238 ResultSet rs = stmt.executeQuery();
1239 try {
1240 return rs.next() ? rs.getString("value") : null;
1241 } finally {
1242 rs.close();
1243 stmt.close();
1244 con.close();
1245 }
1246 } catch(SQLException e) {
1247 throw new RuntimeException(e);
1248 }
1249 }
1250 });
1251
1252 public String getProperty(String key) {
1253 return propertyCache.get(key);
1254 }
1255
1256 public void setProperty(String key,String value) {
1257 try {
1258 Connection con = db().getConnection();
1259 PreparedStatement stmt = con.prepareStatement(
1260 "delete from user_property where user_id = ? and key = ?"
1261 );
1262 stmt.setLong( 1, getId() );
1263 stmt.setString( 2, key );
1264 stmt.executeUpdate();
1265 stmt.close();
1266 if( value != null ) {
1267 stmt = con.prepareStatement(
1268 "insert into user_property (user_id,key,value) values (?,?,?)"
1269 );
1270 stmt.setLong( 1, getId() );
1271 stmt.setString( 2, key );
1272 stmt.setString( 3, value );
1273 stmt.executeUpdate();
1274 stmt.close();
1275 }
1276 con.close();
1277 } catch(SQLException e) {
1278 throw new RuntimeException(e);
1279 } finally {
1280 propertyCache.remove(key);
1281 }
1282 }
1283
1284
1285 final Memoizer<String,Boolean> tagCache = new Memoizer<String,Boolean>(new Computable<String,Boolean>() {
1286 public Boolean get(String sqlCondition) {
1287 return TagImpl.countTags(siteKey,sqlCondition) > 0;
1288 }
1289 });
1290
1291
1292
1293
1294
1295 private final static PooledStringDigester passwordDigester = new PooledStringDigester();
1296
1297 static {
1298 passwordDigester.setAlgorithm(Init.get("passwordDigestAlgorithm","SHA-256"));
1299 passwordDigester.setIterations(Init.get("passwordDigestIterations",100000));
1300 passwordDigester.setSaltSizeBytes(Init.get("passwordDigestSaltSize",16));
1301 passwordDigester.setPoolSize(Init.get("passwordDigestPoolSize",4));
1302 passwordDigester.initialize();
1303 }
1304
1305 private final static PooledStringDigester passcookieDigester = new PooledStringDigester();
1306
1307 static {
1308 passcookieDigester.setAlgorithm(Init.get("passcookieDigestAlgorithm","SHA-256"));
1309 passcookieDigester.setIterations(Init.get("passcookieDigestIterations",100000));
1310 FixedByteArraySaltGenerator sg = new FixedByteArraySaltGenerator();
1311 // this fixed salt needs to be kept secret
1312 sg.setSalt(Init.get("passcookieSalt", new byte[]{105, 4, 40, 78, 24, 46, 30, 100, 18, -27, 114, -21, -44, -59, 103, 43}));
1313 passcookieDigester.setSaltGenerator(sg);
1314 passcookieDigester.setPoolSize(Init.get("passcookieDigestPoolSize",4));
1315 passcookieDigester.initialize();
1316 }
1317
1318 private final static PooledStringDigester resetcodeDigester = new PooledStringDigester();
1319
1320 static {
1321 resetcodeDigester.setAlgorithm(Init.get("resetcodeDigestAlgorithm","SHA-256"));
1322 resetcodeDigester.setIterations(Init.get("resetcodeDigestIterations",100000));
1323 FixedByteArraySaltGenerator sg = new FixedByteArraySaltGenerator();
1324 // this fixed salt needs to be kept secret
1325 sg.setSalt(Init.get("resetcodeSalt", new byte[]{-47, 9, -128, 109, 112, -88, -91, 39, 77, 111, 57, -102, 120, 12, 54, 16}));
1326 resetcodeDigester.setSaltGenerator(sg);
1327 resetcodeDigester.setPoolSize(Init.get("resetcodeDigestPoolSize",4));
1328 resetcodeDigester.initialize();
1329 }
1330
1331 public boolean checkPassword(String password) {
1332 return passwordDigest!=null && passwordDigester.matches(password, passwordDigest);
1333 }
1334
1335 private String calcPasscookie() {
1336 return passcookieDigester.digest(passwordDigest);
1337 }
1338
1339 public boolean checkPasscookie(String passcookie) {
1340 return passwordDigest!=null && getPasscookie().equals(passcookie);
1341 }
1342
1343 public String getResetcode() {
1344 return resetcodeDigester.digest(passwordDigest);
1345 }
1346
1347 public boolean checkResetcode(String resetcode) {
1348 return passwordDigest!=null && getResetcode().equals(resetcode);
1349 }
1350
1351 private static String digestPassword(String password) {
1352 return passwordDigester.digest(password);
1353 }
1354
1355 }