Personal wiki notebook (not under development)
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

User.py 19KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523
  1. import sha
  2. import random
  3. from copy import copy
  4. from Persistent import Persistent, quote
  5. from Notebook import Notebook
  6. class User( Persistent ):
  7. """
  8. A Luminotes user.
  9. """
  10. SALT_CHARS = [ chr( c ) for c in range( ord( "!" ), ord( "~" ) + 1 ) if c != ord( "\\" ) ]
  11. SALT_SIZE = 12
  12. def __init__( self, object_id, revision = None, username = None, salt = None, password_hash = None,
  13. email_address = None, storage_bytes = None, rate_plan = None ):
  14. """
  15. Create a new user with the given credentials and information.
  16. @type object_id: unicode
  17. @param object_id: id of the user
  18. @type revision: datetime or NoneType
  19. @param revision: revision timestamp of the object (optional, defaults to now)
  20. @type username: unicode or NoneType
  21. @param username: unique user identifier for login purposes (optional)
  22. @type salt: unicode or NoneType
  23. @param salt: salt to use when hashing the password (optional, defaults to random)
  24. @type password_hash: unicode or NoneType
  25. @param password_hash: cryptographic hash of secret password for login purposes (optional)
  26. @type email_address: unicode or NoneType
  27. @param email_address: a hopefully valid email address (optional)
  28. @type storage_bytes: int or NoneType
  29. @param storage_bytes: count of bytes that the user is currently using for storage (optional)
  30. @type rate_plan: int or NoneType
  31. @param rate_plan: index into the rate plan array in config/Common.py (optional, defaults to 0)
  32. @rtype: User
  33. @return: newly created user
  34. """
  35. Persistent.__init__( self, object_id, revision )
  36. self.__username = username
  37. self.__salt = salt
  38. self.__password_hash = password_hash
  39. self.__email_address = email_address
  40. self.__storage_bytes = storage_bytes or 0
  41. self.__group_storage_bytes = 0
  42. self.__rate_plan = rate_plan or 0
  43. @staticmethod
  44. def create( object_id, username = None, password = None, email_address = None, rate_plan = None ):
  45. """
  46. Convenience constructor for creating a new user.
  47. @type object_id: unicode
  48. @param object_id: id of the user
  49. @type username: unicode or NoneType
  50. @param username: unique user identifier for login purposes (optional)
  51. @type password: unicode or NoneType
  52. @param password: secret password for login purposes (optional)
  53. @type email_address: unicode or NoneType
  54. @param email_address: a hopefully valid email address (optional)
  55. @type rate_plan: int or NoneType
  56. @param rate_plan: index into the rate plan array in config/Common.py (optional, defaults to 0)
  57. @rtype: User
  58. @return: newly created user
  59. """
  60. salt = User.__create_salt()
  61. password_hash = User.__hash_password( salt, password )
  62. return User( object_id, None, username, salt, password_hash, email_address, rate_plan = rate_plan )
  63. @staticmethod
  64. def __create_salt():
  65. return "".join( [ random.choice( User.SALT_CHARS ) for i in range( User.SALT_SIZE ) ] )
  66. @staticmethod
  67. def __hash_password( salt, password ):
  68. if password is None or len( password ) == 0:
  69. return None
  70. return sha.new( ( salt + password ).encode( "utf8" ) ).hexdigest()
  71. def check_password( self, password ):
  72. """
  73. Check that the given password matches this user's password.
  74. @type password: unicode
  75. @param password: password to check
  76. @rtype: bool
  77. @return: True if the password matches
  78. """
  79. if self.__password_hash == None:
  80. return False
  81. hash = User.__hash_password( self.__salt, password )
  82. if hash == self.__password_hash:
  83. return True
  84. return False
  85. @staticmethod
  86. def sql_load( object_id, revision = None ):
  87. if revision:
  88. return "select * from luminotes_user where id = %s and revision = %s;" % ( quote( object_id ), quote( revision ) )
  89. return "select * from luminotes_user_current where id = %s;" % quote( object_id )
  90. @staticmethod
  91. def sql_id_exists( object_id, revision = None ):
  92. if revision:
  93. return "select id from luminotes_user where id = %s and revision = %s;" % ( quote( object_id ), quote( revision ) )
  94. return "select id from luminotes_user_current where id = %s;" % quote( object_id )
  95. def sql_exists( self ):
  96. return User.sql_id_exists( self.object_id, self.revision )
  97. def sql_create( self ):
  98. return \
  99. "insert into luminotes_user ( id, revision, username, salt, password_hash, email_address, storage_bytes, rate_plan ) " + \
  100. "values ( %s, %s, %s, %s, %s, %s, %s, %s );" % \
  101. ( quote( self.object_id ), quote( self.revision ), quote( self.__username ),
  102. quote( self.__salt ), quote( self.__password_hash ), quote( self.__email_address ),
  103. self.__storage_bytes, self.__rate_plan )
  104. def sql_update( self ):
  105. return self.sql_create()
  106. @staticmethod
  107. def sql_load_by_username( username ):
  108. return "select * from luminotes_user_current where username = %s;" % quote( username )
  109. @staticmethod
  110. def sql_load_by_email_address( email_address ):
  111. return "select * from luminotes_user_current where email_address = %s;" % quote( email_address )
  112. def sql_load_notebooks( self, parents_only = False, undeleted_only = False, read_write = False,
  113. tag_name = None, tag_value = None, notebook_id = None,
  114. exclude_notebook_name = None, start = 0, count = None, reverse = False ):
  115. """
  116. Return a SQL string to load a list of the notebooks to which this user has access.
  117. """
  118. if parents_only:
  119. parents_only_clause = " and trash_id is not null"
  120. else:
  121. parents_only_clause = ""
  122. if undeleted_only:
  123. undeleted_only_clause = " and deleted = 'f'"
  124. else:
  125. undeleted_only_clause = ""
  126. if read_write:
  127. read_write_clause = " and user_notebook.read_write = 't'"
  128. else:
  129. read_write_clause = ""
  130. if tag_name:
  131. tag_tables = ", tag_notebook, tag"
  132. tag_clause = \
  133. """
  134. and tag_notebook.tag_id = tag.id and tag_notebook.user_id = %s and
  135. tag_notebook.notebook_id = notebook_current.id and tag.name = %s
  136. """ % ( quote( self.object_id ), quote( tag_name ) )
  137. if tag_value:
  138. tag_clause += " and tag_notebook.value = %s" % quote( tag_value )
  139. else:
  140. tag_tables = ""
  141. tag_clause = ""
  142. # useful for loading just a single notebook that the user has access to
  143. if notebook_id:
  144. notebook_id_clause = " and notebook_current.id = %s" % quote( notebook_id )
  145. else:
  146. notebook_id_clause = ""
  147. if exclude_notebook_name:
  148. notebook_name_clause = " and not notebook_current.name = %s" % quote( exclude_notebook_name )
  149. else:
  150. notebook_name_clause = ""
  151. if reverse:
  152. ordering = u"desc"
  153. else:
  154. ordering = u"asc"
  155. if count is not None:
  156. limit_clause = " limit %s" % count
  157. else:
  158. limit_clause = ""
  159. if start:
  160. offset_clause = " offset %s" % start
  161. else:
  162. offset_clause = ""
  163. return \
  164. """
  165. select
  166. notebook_current.*, user_notebook.read_write, user_notebook.owner, user_notebook.rank, user_notebook.own_notes_only,
  167. ( select count( note_current.id )
  168. from note_current
  169. where note_current.notebook_id = notebook_current.id and
  170. note_current.deleted_from_id is null )
  171. from
  172. user_notebook, notebook_current%s
  173. where
  174. user_notebook.user_id = %s%s%s%s%s%s%s and
  175. user_notebook.notebook_id = notebook_current.id
  176. order by user_notebook.rank, notebook_current.revision %s%s%s;
  177. """ % ( tag_tables, quote( self.object_id ), parents_only_clause, undeleted_only_clause,
  178. read_write_clause, tag_clause, notebook_id_clause, notebook_name_clause, ordering,
  179. limit_clause, offset_clause )
  180. def sql_count_notebooks( self, parents_only = False, undeleted_only = False, read_write = False,
  181. tag_name = None, tag_value = None, exclude_notebook_name = None ):
  182. """
  183. Return a SQL string to count the number notebooks to which this user has access.
  184. """
  185. if parents_only:
  186. parents_only_clause = " and trash_id is not null"
  187. else:
  188. parents_only_clause = ""
  189. if undeleted_only:
  190. undeleted_only_clause = " and deleted = 'f'"
  191. else:
  192. undeleted_only_clause = ""
  193. if read_write:
  194. read_write_clause = " and user_notebook.read_write = 't'"
  195. else:
  196. read_write_clause = ""
  197. if tag_name:
  198. tag_tables = ", tag_notebook, tag"
  199. tag_clause = \
  200. """
  201. and tag_notebook.tag_id = tag.id and tag_notebook.user_id = %s and
  202. tag_notebook.notebook_id = notebook_current.id and tag.name = %s
  203. """ % ( quote( self.object_id ), quote( tag_name ) )
  204. if tag_value:
  205. tag_clause += " and tag_notebook.value = %s" % quote( tag_value )
  206. else:
  207. tag_tables = ""
  208. tag_clause = ""
  209. if exclude_notebook_name:
  210. notebook_name_clause = " and not notebook_current.name = %s" % quote( exclude_notebook_name )
  211. else:
  212. notebook_name_clause = ""
  213. return \
  214. """
  215. select
  216. count( notebook_current.id )
  217. from
  218. user_notebook, notebook_current%s
  219. where
  220. user_notebook.user_id = %s%s%s%s%s%s and
  221. user_notebook.notebook_id = notebook_current.id;
  222. """ % ( tag_tables, quote( self.object_id ), parents_only_clause, undeleted_only_clause,
  223. read_write_clause, tag_clause, notebook_name_clause )
  224. def sql_save_notebook( self, notebook_id, read_write = True, owner = True, rank = None, own_notes_only = False ):
  225. """
  226. Return a SQL string to save the id of a notebook to which this user has access.
  227. """
  228. if rank is None: rank = quote( None )
  229. return \
  230. "insert into user_notebook ( user_id, notebook_id, read_write, owner, rank, own_notes_only ) values " + \
  231. "( %s, %s, %s, %s, %s, %s );" % (
  232. quote( self.object_id ),
  233. quote( notebook_id ),
  234. quote( read_write and 't' or 'f' ),
  235. quote( owner and 't' or 'f' ),
  236. rank,
  237. quote( own_notes_only and 't' or 'f' ),
  238. )
  239. def sql_remove_notebook( self, notebook_id ):
  240. """
  241. Return a SQL string to remove this user's access to a particular notebook.
  242. """
  243. return \
  244. "delete from user_notebook where user_id = %s and notebook_id = %s;" % ( quote( self.object_id ), quote( notebook_id ) )
  245. def sql_has_access( self, notebook_id, read_write = False, owner = False ):
  246. """
  247. Return a SQL string to determine whether this user has access to the given notebook.
  248. """
  249. if read_write is True and owner is True:
  250. return \
  251. "select user_id from user_notebook where user_id = %s and notebook_id = %s and read_write = 't' and owner = 't';" % \
  252. ( quote( self.object_id ), quote( notebook_id ) )
  253. elif read_write is True:
  254. return \
  255. "select user_id from user_notebook where user_id = %s and notebook_id = %s and read_write = 't';" % \
  256. ( quote( self.object_id ), quote( notebook_id ) )
  257. elif owner is True:
  258. return \
  259. "select user_id from user_notebook where user_id = %s and notebook_id = %s and owner = 't';" % \
  260. ( quote( self.object_id ), quote( notebook_id ) )
  261. else:
  262. return \
  263. "select user_id from user_notebook where user_id = %s and notebook_id = %s;" % \
  264. ( quote( self.object_id ), quote( notebook_id ) )
  265. def sql_update_access( self, notebook_id, read_write = Notebook.READ_ONLY, owner = False ):
  266. """
  267. Return a SQL string to update the user's notebook access to the given read_write and owner level.
  268. """
  269. return \
  270. "update user_notebook set read_write = %s, owner = %s, own_notes_only = %s where user_id = %s and notebook_id = %s;" % (
  271. quote( ( read_write not in ( Notebook.READ_ONLY, False ) ) and 't' or 'f' ),
  272. quote( owner and 't' or 'f' ),
  273. quote( ( read_write == Notebook.READ_WRITE_FOR_OWN_NOTES ) and 't' or 'f' ),
  274. quote( self.object_id ),
  275. quote( notebook_id ),
  276. )
  277. def sql_save_notebook_tag( self, notebook_id, tag_id, value = None ):
  278. """
  279. Return a SQL string to associate a tag with a notebook of this user.
  280. """
  281. return \
  282. "insert into tag_notebook ( notebook_id, tag_id, value, user_id ) values " + \
  283. "( %s, %s, %s, %s );" % ( quote( notebook_id ), quote( tag_id ), quote( value ), quote( self.object_id ) )
  284. def sql_update_notebook_rank( self, notebook_id, rank ):
  285. """
  286. Return a SQL string to update the user's rank for the given notebook.
  287. """
  288. return \
  289. "update user_notebook set rank = %s where user_id = %s and notebook_id = %s;" % \
  290. ( quote( rank ), quote( self.object_id ), quote( notebook_id ) )
  291. def sql_highest_notebook_rank( self ):
  292. """
  293. Return a SQL string to determine the highest numbered rank of all notebooks the user has access to."
  294. """
  295. return "select coalesce( max( rank ), -1 ) from user_notebook where user_id = %s;" % quote( self.object_id )
  296. def sql_load_groups( self ):
  297. """
  298. Return a SQL string to load a list of the groups to which this user has membership.
  299. """
  300. return \
  301. """
  302. select
  303. luminotes_group_current.*, user_group.admin
  304. from
  305. user_group, luminotes_group_current
  306. where
  307. user_group.user_id = %s and
  308. user_group.group_id = luminotes_group_current.id
  309. order by luminotes_group_current.name;
  310. """ % quote( self.object_id )
  311. def sql_save_group( self, group_id, admin = False ):
  312. """
  313. Return a SQL string to save the id of a group to which this user has membership.
  314. """
  315. return \
  316. "insert into user_group ( user_id, group_id, admin ) values " + \
  317. "( %s, %s, %s );" % ( quote( self.object_id ), quote( group_id ), quote( admin and 't' or 'f' ) )
  318. def sql_remove_group( self, group_id ):
  319. """
  320. Return a SQL string to remove this user's membership to a particular group.
  321. """
  322. return \
  323. "delete from user_group where user_id = %s and group_id = %s;" % ( quote( self.object_id ), quote( group_id ) )
  324. def sql_in_group( self, group_id, admin = False ):
  325. """
  326. Return a SQL string to determine whether this has membership to the given group.
  327. """
  328. if admin is True:
  329. return \
  330. "select user_id from user_group where user_id = %s and group_id = %s and admin = 't';" % \
  331. ( quote( self.object_id ), quote( group_id ) )
  332. else:
  333. return \
  334. "select user_id from user_group where user_id = %s and group_id = %s;" % \
  335. ( quote( self.object_id ), quote( group_id ) )
  336. def sql_update_group_admin( self, group_id, admin = False ):
  337. """
  338. Return a SQL string to update the user's group membership to have the given admin flag.
  339. """
  340. return \
  341. "update user_group set admin = %s where user_id = %s and group_id = %s;" % \
  342. ( quote( admin and 't' or 'f' ), quote( self.object_id ), quote( group_id ) )
  343. @staticmethod
  344. def sql_revoke_invite_access( notebook_id, trash_id, email_address ):
  345. return \
  346. """
  347. delete from
  348. user_notebook
  349. where
  350. notebook_id in ( %s, %s ) and
  351. user_notebook.user_id in (
  352. select
  353. redeemed_user_id
  354. from
  355. invite
  356. where
  357. notebook_id = %s and
  358. email_address = %s
  359. );
  360. """ % ( quote( notebook_id ), quote( trash_id ), quote( notebook_id ), quote( email_address ) )
  361. def sql_calculate_storage( self, database_backend ):
  362. """
  363. Return a SQL string to calculate the total bytes of storage usage by this user. This includes
  364. storage for all the user's notes (including past revisions) and their uploaded files. It does
  365. not include storage for the notebooks themselves.
  366. """
  367. if database_backend == Persistent.POSTGRESQL_BACKEND:
  368. # this counts bytes for the contents of each column
  369. note_size_clause = "pg_column_size( note.* )"
  370. else:
  371. # this isn't perfect, because length() counts UTF-8 characters instead of bytes.
  372. # some columns are left out because they can be null, which screws up the addition
  373. note_size_clause = \
  374. """
  375. length( note.id ) + length( note.revision ) + length( note.title ) + length( note.contents ) +
  376. length( note.notebook_id ) + length( note.startup ) + length( note.user_id )
  377. """
  378. return \
  379. """
  380. select * from (
  381. select
  382. coalesce( sum( %s ), 0 )
  383. from
  384. user_notebook, note
  385. where
  386. user_notebook.user_id = %s and
  387. user_notebook.owner = 't' and
  388. note.notebook_id = user_notebook.notebook_id
  389. ) as note_storage,
  390. (
  391. select
  392. coalesce( sum( file.size_bytes ), 0 )
  393. from
  394. user_notebook, file
  395. where
  396. user_notebook.user_id = %s and
  397. user_notebook.owner = 't' and
  398. file.notebook_id = user_notebook.notebook_id
  399. ) as file_storage;
  400. """ % ( note_size_clause, quote( self.object_id ), quote( self.object_id ) )
  401. def sql_calculate_group_storage( self ):
  402. """
  403. Return a SQL string to calculate the total bytes of storage usage for all groups that this user
  404. is a member of. This includes the cumulative storage of all users in these groups.
  405. """
  406. return \
  407. """
  408. select coalesce( sum( storage_bytes ), 0 ) from (
  409. select
  410. distinct user_id, storage_bytes
  411. from
  412. user_group, luminotes_user_current
  413. where
  414. group_id in (
  415. select
  416. group_id
  417. from
  418. user_group
  419. where
  420. user_id = %s
  421. ) and
  422. user_id = luminotes_user_current.id
  423. ) as sub;
  424. """ % quote( self.object_id )
  425. def to_dict( self ):
  426. d = Persistent.to_dict( self )
  427. d.update( dict(
  428. username = self.username,
  429. email_address = self.__email_address,
  430. storage_bytes = self.__storage_bytes,
  431. group_storage_bytes = self.__group_storage_bytes,
  432. rate_plan = self.__rate_plan,
  433. ) )
  434. return d
  435. def __set_email_address( self, email_address ):
  436. self.update_revision()
  437. self.__email_address = email_address
  438. def __set_password( self, password ):
  439. self.update_revision()
  440. self.__salt = User.__create_salt()
  441. self.__password_hash = User.__hash_password( self.__salt, password )
  442. def __set_storage_bytes( self, storage_bytes ):
  443. self.update_revision()
  444. self.__storage_bytes = storage_bytes
  445. def __set_group_storage_bytes( self, group_storage_bytes ):
  446. # The group_storage_bytes member isn't actually saved to the database, so setting it doesn't
  447. # need to call update_revision().
  448. self.__group_storage_bytes = group_storage_bytes
  449. def __set_rate_plan( self, rate_plan ):
  450. self.update_revision()
  451. self.__rate_plan = rate_plan
  452. username = property( lambda self: self.__username )
  453. email_address = property( lambda self: self.__email_address, __set_email_address )
  454. password = property( None, __set_password )
  455. storage_bytes = property( lambda self: self.__group_storage_bytes or self.__storage_bytes, __set_storage_bytes )
  456. group_storage_bytes = property( lambda self: self.__group_storage_bytes, __set_group_storage_bytes )
  457. rate_plan = property( lambda self: self.__rate_plan, __set_rate_plan )