comparison roundup/cgi/client.py @ 1003:f89b8d32291b

Hack hack hack... . Implemented security assertion idea punted to mailing list (pretty easy to back out if someone comes up with a better idea) so editing "my details" works again. Rationalised and cleaned up the actions in any case. . fixed some more display issues (stuff appearing when it should and shouldn't) . trying a nicer colouring scheme for the top level page . handle no grouping being specified . fixed journaltag so the logged-in user is journalled, not admin!
author Richard Jones <richard@users.sourceforge.net>
date Sun, 01 Sep 2002 12:18:41 +0000
parents 1798d2fa9fec
children 5f12d3259f31
comparison
equal deleted inserted replaced
1002:1798d2fa9fec 1003:f89b8d32291b
1 # $Id: client.py,v 1.2 2002-09-01 04:32:30 richard Exp $ 1 # $Id: client.py,v 1.3 2002-09-01 12:18:40 richard Exp $
2 2
3 __doc__ = """ 3 __doc__ = """
4 WWW request handler (also used in the stand-alone server). 4 WWW request handler (also used in the stand-alone server).
5 """ 5 """
6 6
169 if user == 'anonymous': 169 if user == 'anonymous':
170 self.make_user_anonymous() 170 self.make_user_anonymous()
171 else: 171 else:
172 self.user = user 172 self.user = user
173 173
174 # reopen the database as the correct user
175 self.opendb(self.user)
176
174 def determine_context(self, dre=re.compile(r'([^\d]+)(\d+)')): 177 def determine_context(self, dre=re.compile(r'([^\d]+)(\d+)')):
175 ''' Determine the context of this page: 178 ''' Determine the context of this page:
176 179
177 home (default if no url is given) 180 home (default if no url is given)
178 classname 181 classname
289 # determine_context 292 # determine_context
290 return self.template(self.template_name) 293 return self.template(self.template_name)
291 294
292 # these are the actions that are available 295 # these are the actions that are available
293 actions = { 296 actions = {
294 'edit': 'edititem_action', 297 'edit': 'editItemAction',
295 'new': 'newitem_action', 298 'new': 'newItemAction',
296 'login': 'login_action', 299 'login': 'login_action',
297 'logout': 'logout_action', 300 'logout': 'logout_action',
298 'register': 'register_action', 301 'register': 'register_action',
299 'search': 'search_action', 302 'search': 'searchAction',
300 } 303 }
301 def handle_action(self): 304 def handle_action(self):
302 ''' Determine whether there should be an _action called. 305 ''' Determine whether there should be an _action called.
303 306
304 The action is defined by the form variable :action which 307 The action is defined by the form variable :action which
305 identifies the method on this object to call. The four basic 308 identifies the method on this object to call. The four basic
306 actions are defined in the "actions" dictionary on this class: 309 actions are defined in the "actions" dictionary on this class:
307 "edit" -> self.edititem_action 310 "edit" -> self.editItemAction
308 "new" -> self.newitem_action 311 "new" -> self.newItemAction
309 "login" -> self.login_action 312 "login" -> self.login_action
310 "logout" -> self.logout_action 313 "logout" -> self.logout_action
311 "register" -> self.register_action 314 "register" -> self.register_action
312 "search" -> self.search_action 315 "search" -> self.searchAction
313 316
314 ''' 317 '''
315 if not self.form.has_key(':action'): 318 if not self.form.has_key(':action'):
316 return None 319 return None
317 try: 320 try:
452 # construct the logout cookie 455 # construct the logout cookie
453 now = Cookie._getdate() 456 now = Cookie._getdate()
454 path = '/'.join((self.env['SCRIPT_NAME'], self.env['INSTANCE_NAME'], 457 path = '/'.join((self.env['SCRIPT_NAME'], self.env['INSTANCE_NAME'],
455 '')) 458 ''))
456 self.header(headers={'Set-Cookie': 459 self.header(headers={'Set-Cookie':
457 'roundup_user=deleted; Max-Age=0; expires=%s; Path=%s;'%(now, path)}) 460 'roundup_user=deleted; Max-Age=0; expires=%s; Path=%s;'%(now, path)})
458 # 'Location': self.db.config.DEFAULT_VIEW}, response=301) 461
459 462 # Let the user know what's going on
460 # suboptimal, but will do for now
461 self.ok_message.append(_('You are logged out')) 463 self.ok_message.append(_('You are logged out'))
462 #raise Redirect, None
463 464
464 def register_action(self): 465 def register_action(self):
465 '''Attempt to create a new user based on the contents of the form 466 '''Attempt to create a new user based on the contents of the form
466 and then set the cookie. 467 and then set the cookie.
467 468
495 self.set_cookie(self.user, password) 496 self.set_cookie(self.user, password)
496 497
497 # nice message 498 # nice message
498 self.ok_message.append(_('You are now registered, welcome!')) 499 self.ok_message.append(_('You are now registered, welcome!'))
499 500
500 def edititem_action(self): 501 def editItemAction(self):
501 ''' Perform an edit of an item in the database. 502 ''' Perform an edit of an item in the database.
502 503
503 Some special form elements: 504 Some special form elements:
504 505
505 :link=designator:property 506 :link=designator:property
514 "files" property. Attach the file to the message created from 515 "files" property. Attach the file to the message created from
515 the __note if it's supplied. 516 the __note if it's supplied.
516 ''' 517 '''
517 cl = self.db.classes[self.classname] 518 cl = self.db.classes[self.classname]
518 519
520 # parse the props from the form
521 try:
522 props = parsePropsFromForm(self.db, cl, self.form, self.nodeid)
523 except (ValueError, KeyError), message:
524 self.error_message.append(_('Error: ') + str(message))
525 return
526
519 # check permission 527 # check permission
520 userid = self.db.user.lookup(self.user) 528 if not self.editItemPermission(props):
521 if not self.db.security.hasPermission('Edit', userid, self.classname):
522 self.error_message.append( 529 self.error_message.append(
523 _('You do not have permission to edit %(classname)s' % 530 _('You do not have permission to edit %(classname)s'%
524 self.__dict__)) 531 self.__dict__))
525 return 532 return
526 533
527 # perform the edit 534 # perform the edit
528 try: 535 try:
529 props = parsePropsFromForm(self.db, cl, self.form, self.nodeid)
530
531 # make changes to the node 536 # make changes to the node
532 props = self._changenode(props) 537 props = self._changenode(props)
533
534 # handle linked nodes 538 # handle linked nodes
535 self._post_editnode(self.nodeid) 539 self._post_editnode(self.nodeid)
536
537 except (ValueError, KeyError), message: 540 except (ValueError, KeyError), message:
538 self.error_message.append(_('Error: ') + str(message)) 541 self.error_message.append(_('Error: ') + str(message))
539 return 542 return
540 543
541 # commit now that all the tricky stuff is done 544 # commit now that all the tricky stuff is done
554 557
555 # redirect to the item's edit page 558 # redirect to the item's edit page
556 raise Redirect, '%s/%s%s?:ok_message=%s'%(self.base, self.classname, 559 raise Redirect, '%s/%s%s?:ok_message=%s'%(self.base, self.classname,
557 self.nodeid, urllib.quote(message)) 560 self.nodeid, urllib.quote(message))
558 561
559 def newitem_action(self): 562 def editItemPermission(self, props):
563 ''' Determine whether the user has permission to edit this item.
564
565 Base behaviour is to check the user can edit this class. If we're
566 editing the "user" class, users are allowed to edit their own
567 details. Unless it's the "roles" property, which requires the
568 special Permission "Web Roles".
569 '''
570 # if this is a user node and the user is editing their own node, then
571 # we're OK
572 has = self.db.security.hasPermission
573 if self.classname == 'user':
574 # reject if someone's trying to edit "roles" and doesn't have the
575 # right permission.
576 if props.has_key('roles') and not has('Web Roles', self.userid,
577 'user'):
578 return 0
579 # if the item being edited is the current user, we're ok
580 if self.nodeid == self.userid:
581 return 1
582 if not self.db.security.hasPermission('Edit', self.userid,
583 self.classname):
584 return 0
585 return 1
586
587 def newItemAction(self):
560 ''' Add a new item to the database. 588 ''' Add a new item to the database.
561 589
562 This follows the same form as the edititem_action 590 This follows the same form as the editItemAction
563 ''' 591 '''
564 # check permission 592 cl = self.db.classes[self.classname]
565 userid = self.db.user.lookup(self.user) 593
566 if not self.db.security.hasPermission('Edit', userid, self.classname): 594 # parse the props from the form
595 try:
596 props = parsePropsFromForm(self.db, cl, self.form, self.nodeid)
597 except (ValueError, KeyError), message:
598 self.error_message.append(_('Error: ') + str(message))
599 return
600
601 if not self.newItemPermission(props):
567 self.error_message.append( 602 self.error_message.append(
568 _('You do not have permission to create %s' %self.classname)) 603 _('You do not have permission to create %s' %self.classname))
569 604
570 # XXX 605 # XXX
571 # cl = self.db.classes[cn] 606 # cl = self.db.classes[cn]
576 # else: 611 # else:
577 # xtra = '' 612 # xtra = ''
578 613
579 try: 614 try:
580 # do the create 615 # do the create
581 nid = self._createnode() 616 nid = self._createnode(props)
582 617
583 # handle linked nodes 618 # handle linked nodes
584 self._post_editnode(nid) 619 self._post_editnode(nid)
585 620
586 # commit now that all the tricky stuff is done 621 # commit now that all the tricky stuff is done
604 639
605 # redirect to the new item's page 640 # redirect to the new item's page
606 raise Redirect, '%s/%s%s?:ok_message=%s'%(self.base, self.classname, 641 raise Redirect, '%s/%s%s?:ok_message=%s'%(self.base, self.classname,
607 nid, urllib.quote(message)) 642 nid, urllib.quote(message))
608 643
609 def genericedit_action(self): 644 def newItemPermission(self, props):
645 ''' Determine whether the user has permission to create (edit) this
646 item.
647
648 Base behaviour is to check the user can edit this class. No
649 additional property checks are made. Additionally, new user items
650 may be created if the user has the "Web Registration" Permission.
651 '''
652 has = self.db.security.hasPermission
653 if self.classname == 'user' and has('Web Registration', self.userid,
654 'user'):
655 return 1
656 if not has('Edit', self.userid, self.classname):
657 return 0
658 return 1
659
660 def genericEditAction(self):
610 ''' Performs an edit of all of a class' items in one go. 661 ''' Performs an edit of all of a class' items in one go.
611 662
612 The "rows" CGI var defines the CSV-formatted entries for the 663 The "rows" CGI var defines the CSV-formatted entries for the
613 class. New nodes are identified by the ID 'X' (or any other 664 class. New nodes are identified by the ID 'X' (or any other
614 non-existent ID) and removed lines are retired. 665 non-existent ID) and removed lines are retired.
615 ''' 666 '''
616 userid = self.db.user.lookup(self.user) 667 # generic edit is per-class only
617 if not self.db.security.hasPermission('Edit', userid, self.classname): 668 if not self.genericEditPermission():
618 raise Unauthorised, _("You do not have permission to access"\ 669 self.error_message.append(
619 " %(action)s.")%{'action': self.classname} 670 _('You do not have permission to edit %s' %self.classname))
620 cl = self.db.classes[self.classname]
621 idlessprops = cl.getprops(protected=0).keys()
622 props = ['id'] + idlessprops
623 671
624 # get the CSV module 672 # get the CSV module
625 try: 673 try:
626 import csv 674 import csv
627 except ImportError: 675 except ImportError:
628 self.error_message.append(_( 676 self.error_message.append(_(
629 'Sorry, you need the csv module to use this function.<br>\n' 677 'Sorry, you need the csv module to use this function.<br>\n'
630 'Get it from: <a href="http://www.object-craft.com.au/projects/csv/">http://www.object-craft.com.au/projects/csv/')) 678 'Get it from: <a href="http://www.object-craft.com.au/projects/csv/">http://www.object-craft.com.au/projects/csv/'))
631 return 679 return
680
681 cl = self.db.classes[self.classname]
682 idlessprops = cl.getprops(protected=0).keys()
683 props = ['id'] + idlessprops
632 684
633 # do the edit 685 # do the edit
634 rows = self.form['rows'].value.splitlines() 686 rows = self.form['rows'].value.splitlines()
635 p = csv.parser() 687 p = csv.parser()
636 found = {} 688 found = {}
678 730
679 # redirect to the class' edit page 731 # redirect to the class' edit page
680 raise Redirect, '%s/%s?:ok_message=%s'%(self.base, self.classname, 732 raise Redirect, '%s/%s?:ok_message=%s'%(self.base, self.classname,
681 urllib.quote(message)) 733 urllib.quote(message))
682 734
683 def _changenode(self, props): 735 def genericEditPermission(self):
684 ''' change the node based on the contents of the form 736 ''' Determine whether the user has permission to edit this class.
685 ''' 737
686 cl = self.db.classes[self.classname] 738 Base behaviour is to check the user can edit this class.
687 739 '''
688 # create the message 740 if not self.db.security.hasPermission('Edit', self.userid,
689 message, files = self._handle_message() 741 self.classname):
690 if message: 742 return 0
691 props['messages'] = cl.get(self.nodeid, 'messages') + [message] 743 return 1
692 if files: 744
693 props['files'] = cl.get(self.nodeid, 'files') + files 745 def searchAction(self):
694
695 # make the changes
696 return cl.set(self.nodeid, **props)
697
698 def _createnode(self):
699 ''' create a node based on the contents of the form
700 '''
701 cl = self.db.classes[self.classname]
702 props = parsePropsFromForm(self.db, cl, self.form)
703
704 # check for messages and files
705 message, files = self._handle_message()
706 if message:
707 props['messages'] = [message]
708 if files:
709 props['files'] = files
710 # create the node and return it's id
711 return cl.create(**props)
712
713 def _handle_message(self):
714 ''' generate an edit message
715 '''
716 # handle file attachments
717 files = []
718 if self.form.has_key('__file'):
719 file = self.form['__file']
720 if file.filename:
721 filename = file.filename.split('\\')[-1]
722 mime_type = mimetypes.guess_type(filename)[0]
723 if not mime_type:
724 mime_type = "application/octet-stream"
725 # create the new file entry
726 files.append(self.db.file.create(type=mime_type,
727 name=filename, content=file.file.read()))
728
729 # we don't want to do a message if none of the following is true...
730 cn = self.classname
731 cl = self.db.classes[self.classname]
732 props = cl.getprops()
733 note = None
734 # in a nutshell, don't do anything if there's no note or there's no
735 # NOSY
736 if self.form.has_key('__note'):
737 note = self.form['__note'].value.strip()
738 if not note:
739 return None, files
740 if not props.has_key('messages'):
741 return None, files
742 if not isinstance(props['messages'], hyperdb.Multilink):
743 return None, files
744 if not props['messages'].classname == 'msg':
745 return None, files
746 if not (self.form.has_key('nosy') or note):
747 return None, files
748
749 # handle the note
750 if '\n' in note:
751 summary = re.split(r'\n\r?', note)[0]
752 else:
753 summary = note
754 m = ['%s\n'%note]
755
756 # handle the messageid
757 # TODO: handle inreplyto
758 messageid = "<%s.%s.%s@%s>"%(time.time(), random.random(),
759 self.classname, self.instance.MAIL_DOMAIN)
760
761 # now create the message, attaching the files
762 content = '\n'.join(m)
763 message_id = self.db.msg.create(author=self.userid,
764 recipients=[], date=date.Date('.'), summary=summary,
765 content=content, files=files, messageid=messageid)
766
767 # update the messages property
768 return message_id, files
769
770 def _post_editnode(self, nid):
771 '''Do the linking part of the node creation.
772
773 If a form element has :link or :multilink appended to it, its
774 value specifies a node designator and the property on that node
775 to add _this_ node to as a link or multilink.
776
777 This is typically used on, eg. the file upload page to indicated
778 which issue to link the file to.
779
780 TODO: I suspect that this and newfile will go away now that
781 there's the ability to upload a file using the issue __file form
782 element!
783 '''
784 cn = self.classname
785 cl = self.db.classes[cn]
786 # link if necessary
787 keys = self.form.keys()
788 for key in keys:
789 if key == ':multilink':
790 value = self.form[key].value
791 if type(value) != type([]): value = [value]
792 for value in value:
793 designator, property = value.split(':')
794 link, nodeid = hyperdb.splitDesignator(designator)
795 link = self.db.classes[link]
796 # take a dupe of the list so we're not changing the cache
797 value = link.get(nodeid, property)[:]
798 value.append(nid)
799 link.set(nodeid, **{property: value})
800 elif key == ':link':
801 value = self.form[key].value
802 if type(value) != type([]): value = [value]
803 for value in value:
804 designator, property = value.split(':')
805 link, nodeid = hyperdb.splitDesignator(designator)
806 link = self.db.classes[link]
807 link.set(nodeid, **{property: nid})
808
809 def search_action(self):
810 ''' Mangle some of the form variables. 746 ''' Mangle some of the form variables.
811 747
812 Set the form ":filter" variable based on the values of the 748 Set the form ":filter" variable based on the values of the
813 filter variables - if they're set to anything other than 749 filter variables - if they're set to anything other than
814 "dontcare" then add them to :filter. 750 "dontcare" then add them to :filter.
815 ''' 751 '''
752 # generic edit is per-class only
753 if not self.searchPermission():
754 self.error_message.append(
755 _('You do not have permission to search %s' %self.classname))
756
816 # add a faked :filter form variable for each filtering prop 757 # add a faked :filter form variable for each filtering prop
817 props = self.db.classes[self.classname].getprops() 758 props = self.db.classes[self.classname].getprops()
818 for key in self.form.keys(): 759 for key in self.form.keys():
819 if not props.has_key(key): continue 760 if not props.has_key(key): continue
820 if not self.form[key].value: continue 761 if not self.form[key].value: continue
821 self.form.value.append(cgi.MiniFieldStorage(':filter', key)) 762 self.form.value.append(cgi.MiniFieldStorage(':filter', key))
822 763
823 def remove_action(self, dre=re.compile(r'([^\d]+)(\d+)')): 764 def searchPermission(self):
765 ''' Determine whether the user has permission to search this class.
766
767 Base behaviour is to check the user can view this class.
768 '''
769 if not self.db.security.hasPermission('View', self.userid,
770 self.classname):
771 return 0
772 return 1
773
774 def XXXremove_action(self, dre=re.compile(r'([^\d]+)(\d+)')):
775 # XXX I believe this could be handled by a regular edit action that
776 # just sets the multilink...
824 # XXX handle this ! 777 # XXX handle this !
825 target = self.index_arg(':target')[0] 778 target = self.index_arg(':target')[0]
826 m = dre.match(target) 779 m = dre.match(target)
827 if m: 780 if m:
828 classname = m.group(1) 781 classname = m.group(1)
844 return func() 797 return func()
845 else: 798 else:
846 raise NotFound, parent 799 raise NotFound, parent
847 else: 800 else:
848 raise NotFound, target 801 raise NotFound, target
802
803 #
804 # Utility methods for editing
805 #
806 def _changenode(self, props):
807 ''' change the node based on the contents of the form
808 '''
809 cl = self.db.classes[self.classname]
810
811 # create the message
812 message, files = self._handle_message()
813 if message:
814 props['messages'] = cl.get(self.nodeid, 'messages') + [message]
815 if files:
816 props['files'] = cl.get(self.nodeid, 'files') + files
817
818 # make the changes
819 return cl.set(self.nodeid, **props)
820
821 def _createnode(self, props):
822 ''' create a node based on the contents of the form
823 '''
824 cl = self.db.classes[self.classname]
825
826 # check for messages and files
827 message, files = self._handle_message()
828 if message:
829 props['messages'] = [message]
830 if files:
831 props['files'] = files
832 # create the node and return it's id
833 return cl.create(**props)
834
835 def _handle_message(self):
836 ''' generate an edit message
837 '''
838 # handle file attachments
839 files = []
840 if self.form.has_key('__file'):
841 file = self.form['__file']
842 if file.filename:
843 filename = file.filename.split('\\')[-1]
844 mime_type = mimetypes.guess_type(filename)[0]
845 if not mime_type:
846 mime_type = "application/octet-stream"
847 # create the new file entry
848 files.append(self.db.file.create(type=mime_type,
849 name=filename, content=file.file.read()))
850
851 # we don't want to do a message if none of the following is true...
852 cn = self.classname
853 cl = self.db.classes[self.classname]
854 props = cl.getprops()
855 note = None
856 # in a nutshell, don't do anything if there's no note or there's no
857 # NOSY
858 if self.form.has_key('__note'):
859 note = self.form['__note'].value.strip()
860 if not note:
861 return None, files
862 if not props.has_key('messages'):
863 return None, files
864 if not isinstance(props['messages'], hyperdb.Multilink):
865 return None, files
866 if not props['messages'].classname == 'msg':
867 return None, files
868 if not (self.form.has_key('nosy') or note):
869 return None, files
870
871 # handle the note
872 if '\n' in note:
873 summary = re.split(r'\n\r?', note)[0]
874 else:
875 summary = note
876 m = ['%s\n'%note]
877
878 # handle the messageid
879 # TODO: handle inreplyto
880 messageid = "<%s.%s.%s@%s>"%(time.time(), random.random(),
881 self.classname, self.instance.MAIL_DOMAIN)
882
883 # now create the message, attaching the files
884 content = '\n'.join(m)
885 message_id = self.db.msg.create(author=self.userid,
886 recipients=[], date=date.Date('.'), summary=summary,
887 content=content, files=files, messageid=messageid)
888
889 # update the messages property
890 return message_id, files
891
892 def _post_editnode(self, nid):
893 '''Do the linking part of the node creation.
894
895 If a form element has :link or :multilink appended to it, its
896 value specifies a node designator and the property on that node
897 to add _this_ node to as a link or multilink.
898
899 This is typically used on, eg. the file upload page to indicated
900 which issue to link the file to.
901
902 TODO: I suspect that this and newfile will go away now that
903 there's the ability to upload a file using the issue __file form
904 element!
905 '''
906 cn = self.classname
907 cl = self.db.classes[cn]
908 # link if necessary
909 keys = self.form.keys()
910 for key in keys:
911 if key == ':multilink':
912 value = self.form[key].value
913 if type(value) != type([]): value = [value]
914 for value in value:
915 designator, property = value.split(':')
916 link, nodeid = hyperdb.splitDesignator(designator)
917 link = self.db.classes[link]
918 # take a dupe of the list so we're not changing the cache
919 value = link.get(nodeid, property)[:]
920 value.append(nid)
921 link.set(nodeid, **{property: value})
922 elif key == ':link':
923 value = self.form[key].value
924 if type(value) != type([]): value = [value]
925 for value in value:
926 designator, property = value.split(':')
927 link, nodeid = hyperdb.splitDesignator(designator)
928 link = self.db.classes[link]
929 link.set(nodeid, **{property: nid})
849 930
850 931
851 def parsePropsFromForm(db, cl, form, nodeid=0, num_re=re.compile('^\d+$')): 932 def parsePropsFromForm(db, cl, form, nodeid=0, num_re=re.compile('^\d+$')):
852 '''Pull properties for the given class out of the form. 933 '''Pull properties for the given class out of the form.
853 ''' 934 '''

Roundup Issue Tracker: http://roundup-tracker.org/