561 If eid is None, the whole cache is dropped""" |
561 If eid is None, the whole cache is dropped""" |
562 if eid is None: |
562 if eid is None: |
563 self.transaction_data.pop('ecache', None) |
563 self.transaction_data.pop('ecache', None) |
564 else: |
564 else: |
565 del self.transaction_data['ecache'][eid] |
565 del self.transaction_data['ecache'][eid] |
566 |
|
567 # Tracking of entity added of removed in the transaction ################## |
|
568 # |
|
569 # Those are function to allows cheap call from client in other process. |
|
570 |
|
571 def deleted_in_transaction(self, eid): |
|
572 """return True if the entity of the given eid is being deleted in the |
|
573 current transaction |
|
574 """ |
|
575 return eid in self.transaction_data.get('pendingeids', ()) |
|
576 |
|
577 def added_in_transaction(self, eid): |
|
578 """return True if the entity of the given eid is being created in the |
|
579 current transaction |
|
580 """ |
|
581 return eid in self.transaction_data.get('neweids', ()) |
|
582 |
|
583 # Operation management #################################################### |
|
584 |
|
585 def add_operation(self, operation, index=None): |
|
586 """add an operation to be executed at the end of the transaction""" |
|
587 if index is None: |
|
588 self.pending_operations.append(operation) |
|
589 else: |
|
590 self.pending_operations.insert(index, operation) |
|
591 |
|
592 # Hooks control ########################################################### |
|
593 |
|
594 def allow_all_hooks_but(self, *categories): |
|
595 return _hooks_control(self, HOOKS_ALLOW_ALL, *categories) |
|
596 |
|
597 def deny_all_hooks_but(self, *categories): |
|
598 return _hooks_control(self, HOOKS_DENY_ALL, *categories) |
|
599 |
|
600 def disable_hook_categories(self, *categories): |
|
601 """disable the given hook categories: |
|
602 |
|
603 - on HOOKS_DENY_ALL mode, ensure those categories are not enabled |
|
604 - on HOOKS_ALLOW_ALL mode, ensure those categories are disabled |
|
605 """ |
|
606 changes = set() |
|
607 self.pruned_hooks_cache.clear() |
|
608 categories = set(categories) |
|
609 if self.hooks_mode is HOOKS_DENY_ALL: |
|
610 enabledcats = self.enabled_hook_cats |
|
611 changes = enabledcats & categories |
|
612 enabledcats -= changes # changes is small hence faster |
|
613 else: |
|
614 disabledcats = self.disabled_hook_cats |
|
615 changes = categories - disabledcats |
|
616 disabledcats |= changes # changes is small hence faster |
|
617 return tuple(changes) |
|
618 |
|
619 def enable_hook_categories(self, *categories): |
|
620 """enable the given hook categories: |
|
621 |
|
622 - on HOOKS_DENY_ALL mode, ensure those categories are enabled |
|
623 - on HOOKS_ALLOW_ALL mode, ensure those categories are not disabled |
|
624 """ |
|
625 changes = set() |
|
626 self.pruned_hooks_cache.clear() |
|
627 categories = set(categories) |
|
628 if self.hooks_mode is HOOKS_DENY_ALL: |
|
629 enabledcats = self.enabled_hook_cats |
|
630 changes = categories - enabledcats |
|
631 enabledcats |= changes # changes is small hence faster |
|
632 else: |
|
633 disabledcats = self.disabled_hook_cats |
|
634 changes = disabledcats & categories |
|
635 disabledcats -= changes # changes is small hence faster |
|
636 return tuple(changes) |
|
637 |
|
638 def is_hook_category_activated(self, category): |
|
639 """return a boolean telling if the given category is currently activated |
|
640 or not |
|
641 """ |
|
642 if self.hooks_mode is HOOKS_DENY_ALL: |
|
643 return category in self.enabled_hook_cats |
|
644 return category not in self.disabled_hook_cats |
|
645 |
|
646 def is_hook_activated(self, hook): |
|
647 """return a boolean telling if the given hook class is currently |
|
648 activated or not |
|
649 """ |
|
650 return self.is_hook_category_activated(hook.category) |
|
651 |
|
652 # Security management ##################################################### |
|
653 |
|
654 def security_enabled(self, read=None, write=None): |
|
655 return _security_enabled(self, read=read, write=write) |
|
656 |
|
657 @property |
|
658 def read_security(self): |
|
659 return self._read_security |
|
660 |
|
661 @read_security.setter |
|
662 def read_security(self, activated): |
|
663 oldmode = self._read_security |
|
664 self._read_security = activated |
|
665 # running_dbapi_query used to detect hooks triggered by a 'dbapi' query |
|
666 # (eg not issued on the session). This is tricky since we the execution |
|
667 # model of a (write) user query is: |
|
668 # |
|
669 # repository.execute (security enabled) |
|
670 # \-> querier.execute |
|
671 # \-> repo.glob_xxx (add/update/delete entity/relation) |
|
672 # \-> deactivate security before calling hooks |
|
673 # \-> WE WANT TO CHECK QUERY NATURE HERE |
|
674 # \-> potentially, other calls to querier.execute |
|
675 # |
|
676 # so we can't rely on simply checking session.read_security, but |
|
677 # recalling the first transition from DEFAULT_SECURITY to something |
|
678 # else (False actually) is not perfect but should be enough |
|
679 # |
|
680 # also reset running_dbapi_query to true when we go back to |
|
681 # DEFAULT_SECURITY |
|
682 self.running_dbapi_query = (oldmode is DEFAULT_SECURITY |
|
683 or activated is DEFAULT_SECURITY) |
|
684 |
|
685 # undo support ############################################################ |
|
686 |
|
687 def ertype_supports_undo(self, ertype): |
|
688 return self.undo_actions and ertype not in NO_UNDO_TYPES |
|
689 |
|
690 def transaction_uuid(self, set=True): |
|
691 uuid = self.transaction_data.get('tx_uuid') |
|
692 if set and uuid is None: |
|
693 self.transaction_data['tx_uuid'] = uuid = uuid4().hex |
|
694 self.repo.system_source.start_undoable_transaction(self, uuid) |
|
695 return uuid |
|
696 |
|
697 def transaction_inc_action_counter(self): |
|
698 num = self.transaction_data.setdefault('tx_action_count', 0) + 1 |
|
699 self.transaction_data['tx_action_count'] = num |
|
700 return num |
|
701 # db-api like interface ################################################### |
|
702 |
|
703 def source_defs(self): |
|
704 return self.repo.source_defs() |
|
705 |
|
706 def describe(self, eid, asdict=False): |
|
707 """return a tuple (type, sourceuri, extid) for the entity with id <eid>""" |
|
708 metas = self.repo.type_and_source_from_eid(eid, self) |
|
709 if asdict: |
|
710 return dict(zip(('type', 'source', 'extid', 'asource'), metas)) |
|
711 # XXX :-1 for cw compat, use asdict=True for full information |
|
712 return metas[:-1] |
|
713 |
|
714 |
|
715 def source_from_eid(self, eid): |
|
716 """return the source where the entity with id <eid> is located""" |
|
717 return self.repo.source_from_eid(eid, self) |
|
718 |
|
719 # resource accessors ###################################################### |
|
720 |
|
721 def system_sql(self, sql, args=None, rollback_on_failure=True): |
|
722 """return a sql cursor on the system database""" |
|
723 if sql.split(None, 1)[0].upper() != 'SELECT': |
|
724 self.mode = 'write' |
|
725 source = self.cnxset.source('system') |
|
726 try: |
|
727 return source.doexec(self, sql, args, rollback=rollback_on_failure) |
|
728 except (source.OperationalError, source.InterfaceError): |
|
729 if not rollback_on_failure: |
|
730 raise |
|
731 source.warning("trying to reconnect") |
|
732 self.cnxset.reconnect(source) |
|
733 return source.doexec(self, sql, args, rollback=rollback_on_failure) |
|
734 |
|
735 def rtype_eids_rdef(self, rtype, eidfrom, eidto): |
|
736 # use type_and_source_from_eid instead of type_from_eid for optimization |
|
737 # (avoid two extra methods call) |
|
738 subjtype = self.repo.type_and_source_from_eid(eidfrom, self)[0] |
|
739 objtype = self.repo.type_and_source_from_eid(eidto, self)[0] |
|
740 return self.vreg.schema.rschema(rtype).rdefs[(subjtype, objtype)] |
|
741 |
|
742 |
|
743 def cnx_attr(attr_name, writable=False): |
|
744 """return a property to forward attribute access to connection. |
|
745 |
|
746 This is to be used by session""" |
|
747 args = {} |
|
748 def attr_from_cnx(session): |
|
749 return getattr(session._cnx, attr_name) |
|
750 args['fget'] = attr_from_cnx |
|
751 if writable: |
|
752 def write_attr(session, value): |
|
753 return setattr(session._cnx, attr_name, value) |
|
754 args['fset'] = write_attr |
|
755 return property(**args) |
|
756 |
|
757 def cnx_meth(meth_name): |
|
758 """return a function forwarding calls to connection. |
|
759 |
|
760 This is to be used by session""" |
|
761 def meth_from_cnx(session, *args, **kwargs): |
|
762 return getattr(session._cnx, meth_name)(*args, **kwargs) |
|
763 return meth_from_cnx |
|
764 |
|
765 |
|
766 class Session(RequestSessionBase): |
|
767 """Repository user session |
|
768 |
|
769 This tie all together: |
|
770 * session id, |
|
771 * user, |
|
772 * connections set, |
|
773 * other session data. |
|
774 |
|
775 About session storage / transactions |
|
776 ------------------------------------ |
|
777 |
|
778 Here is a description of internal session attributes. Besides :attr:`data` |
|
779 and :attr:`transaction_data`, you should not have to use attributes |
|
780 described here but higher level APIs. |
|
781 |
|
782 :attr:`data` is a dictionary containing shared data, used to communicate |
|
783 extra information between the client and the repository |
|
784 |
|
785 :attr:`_cnxs` is a dictionary of :class:`Connection` instance, one |
|
786 for each running connection. The key is the connection id. By default |
|
787 the connection id is the thread name but it can be otherwise (per dbapi |
|
788 cursor for instance, or per thread name *from another process*). |
|
789 |
|
790 :attr:`__threaddata` is a thread local storage whose `cnx` attribute |
|
791 refers to the proper instance of :class:`Connection` according to the |
|
792 connection. |
|
793 |
|
794 You should not have to use neither :attr:`_cnx` nor :attr:`__threaddata`, |
|
795 simply access connection data transparently through the :attr:`_cnx` |
|
796 property. Also, you usually don't have to access it directly since current |
|
797 connection's data may be accessed/modified through properties / methods: |
|
798 |
|
799 :attr:`connection_data`, similarly to :attr:`data`, is a dictionary |
|
800 containing some shared data that should be cleared at the end of the |
|
801 connection. Hooks and operations may put arbitrary data in there, and |
|
802 this may also be used as a communication channel between the client and |
|
803 the repository. |
|
804 |
|
805 .. automethod:: cubicweb.server.session.Session.get_shared_data |
|
806 .. automethod:: cubicweb.server.session.Session.set_shared_data |
|
807 .. automethod:: cubicweb.server.session.Session.added_in_transaction |
|
808 .. automethod:: cubicweb.server.session.Session.deleted_in_transaction |
|
809 |
|
810 Connection state information: |
|
811 |
|
812 :attr:`running_dbapi_query`, boolean flag telling if the executing query |
|
813 is coming from a dbapi connection or is a query from within the repository |
|
814 |
|
815 :attr:`cnxset`, the connections set to use to execute queries on sources. |
|
816 During a transaction, the connection set may be freed so that is may be |
|
817 used by another session as long as no writing is done. This means we can |
|
818 have multiple sessions with a reasonably low connections set pool size. |
|
819 |
|
820 .. automethod:: cubicweb.server.session.set_cnxset |
|
821 .. automethod:: cubicweb.server.session.free_cnxset |
|
822 |
|
823 :attr:`mode`, string telling the connections set handling mode, may be one |
|
824 of 'read' (connections set may be freed), 'write' (some write was done in |
|
825 the connections set, it can't be freed before end of the transaction), |
|
826 'transaction' (we want to keep the connections set during all the |
|
827 transaction, with or without writing) |
|
828 |
|
829 :attr:`pending_operations`, ordered list of operations to be processed on |
|
830 commit/rollback |
|
831 |
|
832 :attr:`commit_state`, describing the transaction commit state, may be one |
|
833 of None (not yet committing), 'precommit' (calling precommit event on |
|
834 operations), 'postcommit' (calling postcommit event on operations), |
|
835 'uncommitable' (some :exc:`ValidationError` or :exc:`Unauthorized` error |
|
836 has been raised during the transaction and so it must be rollbacked). |
|
837 |
|
838 .. automethod:: cubicweb.server.session.Session.commit |
|
839 .. automethod:: cubicweb.server.session.Session.rollback |
|
840 .. automethod:: cubicweb.server.session.Session.close |
|
841 .. automethod:: cubicweb.server.session.Session.closed |
|
842 |
|
843 Security level Management: |
|
844 |
|
845 :attr:`read_security` and :attr:`write_security`, boolean flags telling if |
|
846 read/write security is currently activated. |
|
847 |
|
848 .. automethod:: cubicweb.server.session.Session.security_enabled |
|
849 |
|
850 Hooks Management: |
|
851 |
|
852 :attr:`hooks_mode`, may be either `HOOKS_ALLOW_ALL` or `HOOKS_DENY_ALL`. |
|
853 |
|
854 :attr:`enabled_hook_categories`, when :attr:`hooks_mode` is |
|
855 `HOOKS_DENY_ALL`, this set contains hooks categories that are enabled. |
|
856 |
|
857 :attr:`disabled_hook_categories`, when :attr:`hooks_mode` is |
|
858 `HOOKS_ALLOW_ALL`, this set contains hooks categories that are disabled. |
|
859 |
|
860 .. automethod:: cubicweb.server.session.Session.deny_all_hooks_but |
|
861 .. automethod:: cubicweb.server.session.Session.allow_all_hooks_but |
|
862 .. automethod:: cubicweb.server.session.Session.is_hook_category_activated |
|
863 .. automethod:: cubicweb.server.session.Session.is_hook_activated |
|
864 |
|
865 Data manipulation: |
|
866 |
|
867 .. automethod:: cubicweb.server.session.Session.add_relation |
|
868 .. automethod:: cubicweb.server.session.Session.add_relations |
|
869 .. automethod:: cubicweb.server.session.Session.delete_relation |
|
870 |
|
871 Other: |
|
872 |
|
873 .. automethod:: cubicweb.server.session.Session.call_service |
|
874 |
|
875 |
|
876 |
|
877 """ |
|
878 is_request = False |
|
879 is_internal_session = False |
|
880 |
|
881 def __init__(self, user, repo, cnxprops=None, _id=None): |
|
882 super(Session, self).__init__(repo.vreg) |
|
883 self.id = _id or make_uid(unormalize(user.login).encode('UTF8')) |
|
884 self.user = user |
|
885 self.repo = repo |
|
886 self.timestamp = time() |
|
887 self.default_mode = 'read' |
|
888 # short cut to querier .execute method |
|
889 self._execute = repo.querier.execute |
|
890 # shared data, used to communicate extra information between the client |
|
891 # and the rql server |
|
892 self.data = {} |
|
893 # i18n initialization |
|
894 self.set_language(user.prefered_language()) |
|
895 ### internals |
|
896 # Connection of this section |
|
897 self._cnxs = {} |
|
898 # Data local to the thread |
|
899 self.__threaddata = threading.local() |
|
900 self._cnxset_tracker = CnxSetTracker() |
|
901 self._closed = False |
|
902 self._lock = threading.RLock() |
|
903 |
|
904 def __unicode__(self): |
|
905 return '<session %s (%s 0x%x)>' % ( |
|
906 unicode(self.user.login), self.id, id(self)) |
|
907 |
|
908 @property |
|
909 def sessionid(self): |
|
910 return self.id |
|
911 |
|
912 @property |
|
913 def login(self): |
|
914 # XXX backward compat with dbapi. deprecate me ASAP. |
|
915 return self.user.login |
|
916 |
|
917 def get_cnx(self, cnxid): |
|
918 """return the <cnxid> connection attached to this session |
|
919 |
|
920 Connection is created if necessary""" |
|
921 with self._lock: # no connection exist with the same id |
|
922 try: |
|
923 if self.closed: |
|
924 raise SessionClosedError('try to access connections set on a closed session %s' % self.id) |
|
925 cnx = self._cnxs[cnxid] |
|
926 except KeyError: |
|
927 rewriter = RQLRewriter(self) |
|
928 cnx = Connection(cnxid, self, rewriter) |
|
929 self._cnxs[cnxid] = cnx |
|
930 return cnx |
|
931 |
|
932 def set_cnx(self, cnxid=None): |
|
933 """set the default connection of the current thread to <cnxid> |
|
934 |
|
935 Connection is created if necessary""" |
|
936 if cnxid is None: |
|
937 cnxid = threading.currentThread().getName() |
|
938 self.__threaddata.cnx = self.get_cnx(cnxid) |
|
939 |
|
940 @property |
|
941 def _cnx(self): |
|
942 """default connection for current session in current thread""" |
|
943 try: |
|
944 return self.__threaddata.cnx |
|
945 except AttributeError: |
|
946 self.set_cnx() |
|
947 return self.__threaddata.cnx |
|
948 |
|
949 @property |
|
950 def _current_cnx_id(self): |
|
951 """TRANSITIONAL PURPOSE""" |
|
952 try: |
|
953 return self.__threaddata.cnx.transactionid |
|
954 except AttributeError: |
|
955 return None |
|
956 |
|
957 def get_option_value(self, option, foreid=None): |
|
958 return self.repo.get_option_value(option, foreid) |
|
959 |
|
960 def transaction(self, free_cnxset=True): |
|
961 """return context manager to enter a transaction for the session: when |
|
962 exiting the `with` block on exception, call `session.rollback()`, else |
|
963 call `session.commit()` on normal exit. |
|
964 |
|
965 The `free_cnxset` will be given to rollback/commit methods to indicate |
|
966 wether the connections set should be freed or not. |
|
967 """ |
|
968 return transaction(self, free_cnxset) |
|
969 |
|
970 def add_relation(self, fromeid, rtype, toeid): |
|
971 """provide direct access to the repository method to add a relation. |
|
972 |
|
973 This is equivalent to the following rql query: |
|
974 |
|
975 SET X rtype Y WHERE X eid fromeid, T eid toeid |
|
976 |
|
977 without read security check but also all the burden of rql execution. |
|
978 You may use this in hooks when you know both eids of the relation you |
|
979 want to add. |
|
980 """ |
|
981 self.add_relations([(rtype, [(fromeid, toeid)])]) |
|
982 |
|
983 def add_relations(self, relations): |
|
984 '''set many relation using a shortcut similar to the one in add_relation |
|
985 |
|
986 relations is a list of 2-uples, the first element of each |
|
987 2-uple is the rtype, and the second is a list of (fromeid, |
|
988 toeid) tuples |
|
989 ''' |
|
990 edited_entities = {} |
|
991 relations_dict = {} |
|
992 with self.security_enabled(False, False): |
|
993 for rtype, eids in relations: |
|
994 if self.vreg.schema[rtype].inlined: |
|
995 for fromeid, toeid in eids: |
|
996 if fromeid not in edited_entities: |
|
997 entity = self.entity_from_eid(fromeid) |
|
998 edited = EditedEntity(entity) |
|
999 edited_entities[fromeid] = edited |
|
1000 else: |
|
1001 edited = edited_entities[fromeid] |
|
1002 edited.edited_attribute(rtype, toeid) |
|
1003 else: |
|
1004 relations_dict[rtype] = eids |
|
1005 self.repo.glob_add_relations(self, relations_dict) |
|
1006 for edited in edited_entities.itervalues(): |
|
1007 self.repo.glob_update_entity(self, edited) |
|
1008 |
|
1009 |
|
1010 def delete_relation(self, fromeid, rtype, toeid): |
|
1011 """provide direct access to the repository method to delete a relation. |
|
1012 |
|
1013 This is equivalent to the following rql query: |
|
1014 |
|
1015 DELETE X rtype Y WHERE X eid fromeid, T eid toeid |
|
1016 |
|
1017 without read security check but also all the burden of rql execution. |
|
1018 You may use this in hooks when you know both eids of the relation you |
|
1019 want to delete. |
|
1020 """ |
|
1021 with self.security_enabled(False, False): |
|
1022 if self.vreg.schema[rtype].inlined: |
|
1023 entity = self.entity_from_eid(fromeid) |
|
1024 entity.cw_attr_cache[rtype] = None |
|
1025 self.repo.glob_update_entity(self, entity, set((rtype,))) |
|
1026 else: |
|
1027 self.repo.glob_delete_relation(self, fromeid, rtype, toeid) |
|
1028 |
566 |
1029 # relations cache handling ################################################# |
567 # relations cache handling ################################################# |
1030 |
568 |
1031 def update_rel_cache_add(self, subject, rtype, object, symmetric=False): |
569 def update_rel_cache_add(self, subject, rtype, object, symmetric=False): |
1032 self._update_entity_rel_cache_add(subject, rtype, 'subject', object) |
570 self._update_entity_rel_cache_add(subject, rtype, 'subject', object) |
1090 del rset.description[idx] |
628 del rset.description[idx] |
1091 del entities[idx] |
629 del entities[idx] |
1092 rset.rowcount -= 1 |
630 rset.rowcount -= 1 |
1093 entity._cw_related_cache['%s_%s' % (rtype, role)] = ( |
631 entity._cw_related_cache['%s_%s' % (rtype, role)] = ( |
1094 rset, tuple(entities)) |
632 rset, tuple(entities)) |
|
633 |
|
634 # Tracking of entity added of removed in the transaction ################## |
|
635 # |
|
636 # Those are function to allows cheap call from client in other process. |
|
637 |
|
638 def deleted_in_transaction(self, eid): |
|
639 """return True if the entity of the given eid is being deleted in the |
|
640 current transaction |
|
641 """ |
|
642 return eid in self.transaction_data.get('pendingeids', ()) |
|
643 |
|
644 def added_in_transaction(self, eid): |
|
645 """return True if the entity of the given eid is being created in the |
|
646 current transaction |
|
647 """ |
|
648 return eid in self.transaction_data.get('neweids', ()) |
|
649 |
|
650 # Operation management #################################################### |
|
651 |
|
652 def add_operation(self, operation, index=None): |
|
653 """add an operation to be executed at the end of the transaction""" |
|
654 if index is None: |
|
655 self.pending_operations.append(operation) |
|
656 else: |
|
657 self.pending_operations.insert(index, operation) |
|
658 |
|
659 # Hooks control ########################################################### |
|
660 |
|
661 def allow_all_hooks_but(self, *categories): |
|
662 return _hooks_control(self, HOOKS_ALLOW_ALL, *categories) |
|
663 |
|
664 def deny_all_hooks_but(self, *categories): |
|
665 return _hooks_control(self, HOOKS_DENY_ALL, *categories) |
|
666 |
|
667 def disable_hook_categories(self, *categories): |
|
668 """disable the given hook categories: |
|
669 |
|
670 - on HOOKS_DENY_ALL mode, ensure those categories are not enabled |
|
671 - on HOOKS_ALLOW_ALL mode, ensure those categories are disabled |
|
672 """ |
|
673 changes = set() |
|
674 self.pruned_hooks_cache.clear() |
|
675 categories = set(categories) |
|
676 if self.hooks_mode is HOOKS_DENY_ALL: |
|
677 enabledcats = self.enabled_hook_cats |
|
678 changes = enabledcats & categories |
|
679 enabledcats -= changes # changes is small hence faster |
|
680 else: |
|
681 disabledcats = self.disabled_hook_cats |
|
682 changes = categories - disabledcats |
|
683 disabledcats |= changes # changes is small hence faster |
|
684 return tuple(changes) |
|
685 |
|
686 def enable_hook_categories(self, *categories): |
|
687 """enable the given hook categories: |
|
688 |
|
689 - on HOOKS_DENY_ALL mode, ensure those categories are enabled |
|
690 - on HOOKS_ALLOW_ALL mode, ensure those categories are not disabled |
|
691 """ |
|
692 changes = set() |
|
693 self.pruned_hooks_cache.clear() |
|
694 categories = set(categories) |
|
695 if self.hooks_mode is HOOKS_DENY_ALL: |
|
696 enabledcats = self.enabled_hook_cats |
|
697 changes = categories - enabledcats |
|
698 enabledcats |= changes # changes is small hence faster |
|
699 else: |
|
700 disabledcats = self.disabled_hook_cats |
|
701 changes = disabledcats & categories |
|
702 disabledcats -= changes # changes is small hence faster |
|
703 return tuple(changes) |
|
704 |
|
705 def is_hook_category_activated(self, category): |
|
706 """return a boolean telling if the given category is currently activated |
|
707 or not |
|
708 """ |
|
709 if self.hooks_mode is HOOKS_DENY_ALL: |
|
710 return category in self.enabled_hook_cats |
|
711 return category not in self.disabled_hook_cats |
|
712 |
|
713 def is_hook_activated(self, hook): |
|
714 """return a boolean telling if the given hook class is currently |
|
715 activated or not |
|
716 """ |
|
717 return self.is_hook_category_activated(hook.category) |
|
718 |
|
719 # Security management ##################################################### |
|
720 |
|
721 def security_enabled(self, read=None, write=None): |
|
722 return _security_enabled(self, read=read, write=write) |
|
723 |
|
724 @property |
|
725 def read_security(self): |
|
726 return self._read_security |
|
727 |
|
728 @read_security.setter |
|
729 def read_security(self, activated): |
|
730 oldmode = self._read_security |
|
731 self._read_security = activated |
|
732 # running_dbapi_query used to detect hooks triggered by a 'dbapi' query |
|
733 # (eg not issued on the session). This is tricky since we the execution |
|
734 # model of a (write) user query is: |
|
735 # |
|
736 # repository.execute (security enabled) |
|
737 # \-> querier.execute |
|
738 # \-> repo.glob_xxx (add/update/delete entity/relation) |
|
739 # \-> deactivate security before calling hooks |
|
740 # \-> WE WANT TO CHECK QUERY NATURE HERE |
|
741 # \-> potentially, other calls to querier.execute |
|
742 # |
|
743 # so we can't rely on simply checking session.read_security, but |
|
744 # recalling the first transition from DEFAULT_SECURITY to something |
|
745 # else (False actually) is not perfect but should be enough |
|
746 # |
|
747 # also reset running_dbapi_query to true when we go back to |
|
748 # DEFAULT_SECURITY |
|
749 self.running_dbapi_query = (oldmode is DEFAULT_SECURITY |
|
750 or activated is DEFAULT_SECURITY) |
|
751 |
|
752 # undo support ############################################################ |
|
753 |
|
754 def ertype_supports_undo(self, ertype): |
|
755 return self.undo_actions and ertype not in NO_UNDO_TYPES |
|
756 |
|
757 def transaction_uuid(self, set=True): |
|
758 uuid = self.transaction_data.get('tx_uuid') |
|
759 if set and uuid is None: |
|
760 self.transaction_data['tx_uuid'] = uuid = uuid4().hex |
|
761 self.repo.system_source.start_undoable_transaction(self, uuid) |
|
762 return uuid |
|
763 |
|
764 def transaction_inc_action_counter(self): |
|
765 num = self.transaction_data.setdefault('tx_action_count', 0) + 1 |
|
766 self.transaction_data['tx_action_count'] = num |
|
767 return num |
|
768 # db-api like interface ################################################### |
|
769 |
|
770 def source_defs(self): |
|
771 return self.repo.source_defs() |
|
772 |
|
773 def describe(self, eid, asdict=False): |
|
774 """return a tuple (type, sourceuri, extid) for the entity with id <eid>""" |
|
775 metas = self.repo.type_and_source_from_eid(eid, self) |
|
776 if asdict: |
|
777 return dict(zip(('type', 'source', 'extid', 'asource'), metas)) |
|
778 # XXX :-1 for cw compat, use asdict=True for full information |
|
779 return metas[:-1] |
|
780 |
|
781 |
|
782 def source_from_eid(self, eid): |
|
783 """return the source where the entity with id <eid> is located""" |
|
784 return self.repo.source_from_eid(eid, self) |
|
785 |
|
786 # resource accessors ###################################################### |
|
787 |
|
788 def system_sql(self, sql, args=None, rollback_on_failure=True): |
|
789 """return a sql cursor on the system database""" |
|
790 if sql.split(None, 1)[0].upper() != 'SELECT': |
|
791 self.mode = 'write' |
|
792 source = self.cnxset.source('system') |
|
793 try: |
|
794 return source.doexec(self, sql, args, rollback=rollback_on_failure) |
|
795 except (source.OperationalError, source.InterfaceError): |
|
796 if not rollback_on_failure: |
|
797 raise |
|
798 source.warning("trying to reconnect") |
|
799 self.cnxset.reconnect(source) |
|
800 return source.doexec(self, sql, args, rollback=rollback_on_failure) |
|
801 |
|
802 def rtype_eids_rdef(self, rtype, eidfrom, eidto): |
|
803 # use type_and_source_from_eid instead of type_from_eid for optimization |
|
804 # (avoid two extra methods call) |
|
805 subjtype = self.repo.type_and_source_from_eid(eidfrom, self)[0] |
|
806 objtype = self.repo.type_and_source_from_eid(eidto, self)[0] |
|
807 return self.vreg.schema.rschema(rtype).rdefs[(subjtype, objtype)] |
|
808 |
|
809 |
|
810 def cnx_attr(attr_name, writable=False): |
|
811 """return a property to forward attribute access to connection. |
|
812 |
|
813 This is to be used by session""" |
|
814 args = {} |
|
815 def attr_from_cnx(session): |
|
816 return getattr(session._cnx, attr_name) |
|
817 args['fget'] = attr_from_cnx |
|
818 if writable: |
|
819 def write_attr(session, value): |
|
820 return setattr(session._cnx, attr_name, value) |
|
821 args['fset'] = write_attr |
|
822 return property(**args) |
|
823 |
|
824 def cnx_meth(meth_name): |
|
825 """return a function forwarding calls to connection. |
|
826 |
|
827 This is to be used by session""" |
|
828 def meth_from_cnx(session, *args, **kwargs): |
|
829 return getattr(session._cnx, meth_name)(*args, **kwargs) |
|
830 return meth_from_cnx |
|
831 |
|
832 |
|
833 class Session(RequestSessionBase): |
|
834 """Repository user session |
|
835 |
|
836 This tie all together: |
|
837 * session id, |
|
838 * user, |
|
839 * connections set, |
|
840 * other session data. |
|
841 |
|
842 About session storage / transactions |
|
843 ------------------------------------ |
|
844 |
|
845 Here is a description of internal session attributes. Besides :attr:`data` |
|
846 and :attr:`transaction_data`, you should not have to use attributes |
|
847 described here but higher level APIs. |
|
848 |
|
849 :attr:`data` is a dictionary containing shared data, used to communicate |
|
850 extra information between the client and the repository |
|
851 |
|
852 :attr:`_cnxs` is a dictionary of :class:`Connection` instance, one |
|
853 for each running connection. The key is the connection id. By default |
|
854 the connection id is the thread name but it can be otherwise (per dbapi |
|
855 cursor for instance, or per thread name *from another process*). |
|
856 |
|
857 :attr:`__threaddata` is a thread local storage whose `cnx` attribute |
|
858 refers to the proper instance of :class:`Connection` according to the |
|
859 connection. |
|
860 |
|
861 You should not have to use neither :attr:`_cnx` nor :attr:`__threaddata`, |
|
862 simply access connection data transparently through the :attr:`_cnx` |
|
863 property. Also, you usually don't have to access it directly since current |
|
864 connection's data may be accessed/modified through properties / methods: |
|
865 |
|
866 :attr:`connection_data`, similarly to :attr:`data`, is a dictionary |
|
867 containing some shared data that should be cleared at the end of the |
|
868 connection. Hooks and operations may put arbitrary data in there, and |
|
869 this may also be used as a communication channel between the client and |
|
870 the repository. |
|
871 |
|
872 .. automethod:: cubicweb.server.session.Session.get_shared_data |
|
873 .. automethod:: cubicweb.server.session.Session.set_shared_data |
|
874 .. automethod:: cubicweb.server.session.Session.added_in_transaction |
|
875 .. automethod:: cubicweb.server.session.Session.deleted_in_transaction |
|
876 |
|
877 Connection state information: |
|
878 |
|
879 :attr:`running_dbapi_query`, boolean flag telling if the executing query |
|
880 is coming from a dbapi connection or is a query from within the repository |
|
881 |
|
882 :attr:`cnxset`, the connections set to use to execute queries on sources. |
|
883 During a transaction, the connection set may be freed so that is may be |
|
884 used by another session as long as no writing is done. This means we can |
|
885 have multiple sessions with a reasonably low connections set pool size. |
|
886 |
|
887 .. automethod:: cubicweb.server.session.set_cnxset |
|
888 .. automethod:: cubicweb.server.session.free_cnxset |
|
889 |
|
890 :attr:`mode`, string telling the connections set handling mode, may be one |
|
891 of 'read' (connections set may be freed), 'write' (some write was done in |
|
892 the connections set, it can't be freed before end of the transaction), |
|
893 'transaction' (we want to keep the connections set during all the |
|
894 transaction, with or without writing) |
|
895 |
|
896 :attr:`pending_operations`, ordered list of operations to be processed on |
|
897 commit/rollback |
|
898 |
|
899 :attr:`commit_state`, describing the transaction commit state, may be one |
|
900 of None (not yet committing), 'precommit' (calling precommit event on |
|
901 operations), 'postcommit' (calling postcommit event on operations), |
|
902 'uncommitable' (some :exc:`ValidationError` or :exc:`Unauthorized` error |
|
903 has been raised during the transaction and so it must be rollbacked). |
|
904 |
|
905 .. automethod:: cubicweb.server.session.Session.commit |
|
906 .. automethod:: cubicweb.server.session.Session.rollback |
|
907 .. automethod:: cubicweb.server.session.Session.close |
|
908 .. automethod:: cubicweb.server.session.Session.closed |
|
909 |
|
910 Security level Management: |
|
911 |
|
912 :attr:`read_security` and :attr:`write_security`, boolean flags telling if |
|
913 read/write security is currently activated. |
|
914 |
|
915 .. automethod:: cubicweb.server.session.Session.security_enabled |
|
916 |
|
917 Hooks Management: |
|
918 |
|
919 :attr:`hooks_mode`, may be either `HOOKS_ALLOW_ALL` or `HOOKS_DENY_ALL`. |
|
920 |
|
921 :attr:`enabled_hook_categories`, when :attr:`hooks_mode` is |
|
922 `HOOKS_DENY_ALL`, this set contains hooks categories that are enabled. |
|
923 |
|
924 :attr:`disabled_hook_categories`, when :attr:`hooks_mode` is |
|
925 `HOOKS_ALLOW_ALL`, this set contains hooks categories that are disabled. |
|
926 |
|
927 .. automethod:: cubicweb.server.session.Session.deny_all_hooks_but |
|
928 .. automethod:: cubicweb.server.session.Session.allow_all_hooks_but |
|
929 .. automethod:: cubicweb.server.session.Session.is_hook_category_activated |
|
930 .. automethod:: cubicweb.server.session.Session.is_hook_activated |
|
931 |
|
932 Data manipulation: |
|
933 |
|
934 .. automethod:: cubicweb.server.session.Session.add_relation |
|
935 .. automethod:: cubicweb.server.session.Session.add_relations |
|
936 .. automethod:: cubicweb.server.session.Session.delete_relation |
|
937 |
|
938 Other: |
|
939 |
|
940 .. automethod:: cubicweb.server.session.Session.call_service |
|
941 |
|
942 |
|
943 |
|
944 """ |
|
945 is_request = False |
|
946 is_internal_session = False |
|
947 |
|
948 def __init__(self, user, repo, cnxprops=None, _id=None): |
|
949 super(Session, self).__init__(repo.vreg) |
|
950 self.id = _id or make_uid(unormalize(user.login).encode('UTF8')) |
|
951 self.user = user |
|
952 self.repo = repo |
|
953 self.timestamp = time() |
|
954 self.default_mode = 'read' |
|
955 # short cut to querier .execute method |
|
956 self._execute = repo.querier.execute |
|
957 # shared data, used to communicate extra information between the client |
|
958 # and the rql server |
|
959 self.data = {} |
|
960 # i18n initialization |
|
961 self.set_language(user.prefered_language()) |
|
962 ### internals |
|
963 # Connection of this section |
|
964 self._cnxs = {} |
|
965 # Data local to the thread |
|
966 self.__threaddata = threading.local() |
|
967 self._cnxset_tracker = CnxSetTracker() |
|
968 self._closed = False |
|
969 self._lock = threading.RLock() |
|
970 |
|
971 def __unicode__(self): |
|
972 return '<session %s (%s 0x%x)>' % ( |
|
973 unicode(self.user.login), self.id, id(self)) |
|
974 |
|
975 @property |
|
976 def sessionid(self): |
|
977 return self.id |
|
978 |
|
979 @property |
|
980 def login(self): |
|
981 # XXX backward compat with dbapi. deprecate me ASAP. |
|
982 return self.user.login |
|
983 |
|
984 def get_cnx(self, cnxid): |
|
985 """return the <cnxid> connection attached to this session |
|
986 |
|
987 Connection is created if necessary""" |
|
988 with self._lock: # no connection exist with the same id |
|
989 try: |
|
990 if self.closed: |
|
991 raise SessionClosedError('try to access connections set on a closed session %s' % self.id) |
|
992 cnx = self._cnxs[cnxid] |
|
993 except KeyError: |
|
994 rewriter = RQLRewriter(self) |
|
995 cnx = Connection(cnxid, self, rewriter) |
|
996 self._cnxs[cnxid] = cnx |
|
997 return cnx |
|
998 |
|
999 def set_cnx(self, cnxid=None): |
|
1000 """set the default connection of the current thread to <cnxid> |
|
1001 |
|
1002 Connection is created if necessary""" |
|
1003 if cnxid is None: |
|
1004 cnxid = threading.currentThread().getName() |
|
1005 self.__threaddata.cnx = self.get_cnx(cnxid) |
|
1006 |
|
1007 @property |
|
1008 def _cnx(self): |
|
1009 """default connection for current session in current thread""" |
|
1010 try: |
|
1011 return self.__threaddata.cnx |
|
1012 except AttributeError: |
|
1013 self.set_cnx() |
|
1014 return self.__threaddata.cnx |
|
1015 |
|
1016 @property |
|
1017 def _current_cnx_id(self): |
|
1018 """TRANSITIONAL PURPOSE""" |
|
1019 try: |
|
1020 return self.__threaddata.cnx.transactionid |
|
1021 except AttributeError: |
|
1022 return None |
|
1023 |
|
1024 def get_option_value(self, option, foreid=None): |
|
1025 return self.repo.get_option_value(option, foreid) |
|
1026 |
|
1027 def transaction(self, free_cnxset=True): |
|
1028 """return context manager to enter a transaction for the session: when |
|
1029 exiting the `with` block on exception, call `session.rollback()`, else |
|
1030 call `session.commit()` on normal exit. |
|
1031 |
|
1032 The `free_cnxset` will be given to rollback/commit methods to indicate |
|
1033 wether the connections set should be freed or not. |
|
1034 """ |
|
1035 return transaction(self, free_cnxset) |
|
1036 |
|
1037 def add_relation(self, fromeid, rtype, toeid): |
|
1038 """provide direct access to the repository method to add a relation. |
|
1039 |
|
1040 This is equivalent to the following rql query: |
|
1041 |
|
1042 SET X rtype Y WHERE X eid fromeid, T eid toeid |
|
1043 |
|
1044 without read security check but also all the burden of rql execution. |
|
1045 You may use this in hooks when you know both eids of the relation you |
|
1046 want to add. |
|
1047 """ |
|
1048 self.add_relations([(rtype, [(fromeid, toeid)])]) |
|
1049 |
|
1050 def add_relations(self, relations): |
|
1051 '''set many relation using a shortcut similar to the one in add_relation |
|
1052 |
|
1053 relations is a list of 2-uples, the first element of each |
|
1054 2-uple is the rtype, and the second is a list of (fromeid, |
|
1055 toeid) tuples |
|
1056 ''' |
|
1057 edited_entities = {} |
|
1058 relations_dict = {} |
|
1059 with self.security_enabled(False, False): |
|
1060 for rtype, eids in relations: |
|
1061 if self.vreg.schema[rtype].inlined: |
|
1062 for fromeid, toeid in eids: |
|
1063 if fromeid not in edited_entities: |
|
1064 entity = self.entity_from_eid(fromeid) |
|
1065 edited = EditedEntity(entity) |
|
1066 edited_entities[fromeid] = edited |
|
1067 else: |
|
1068 edited = edited_entities[fromeid] |
|
1069 edited.edited_attribute(rtype, toeid) |
|
1070 else: |
|
1071 relations_dict[rtype] = eids |
|
1072 self.repo.glob_add_relations(self, relations_dict) |
|
1073 for edited in edited_entities.itervalues(): |
|
1074 self.repo.glob_update_entity(self, edited) |
|
1075 |
|
1076 |
|
1077 def delete_relation(self, fromeid, rtype, toeid): |
|
1078 """provide direct access to the repository method to delete a relation. |
|
1079 |
|
1080 This is equivalent to the following rql query: |
|
1081 |
|
1082 DELETE X rtype Y WHERE X eid fromeid, T eid toeid |
|
1083 |
|
1084 without read security check but also all the burden of rql execution. |
|
1085 You may use this in hooks when you know both eids of the relation you |
|
1086 want to delete. |
|
1087 """ |
|
1088 with self.security_enabled(False, False): |
|
1089 if self.vreg.schema[rtype].inlined: |
|
1090 entity = self.entity_from_eid(fromeid) |
|
1091 entity.cw_attr_cache[rtype] = None |
|
1092 self.repo.glob_update_entity(self, entity, set((rtype,))) |
|
1093 else: |
|
1094 self.repo.glob_delete_relation(self, fromeid, rtype, toeid) |
|
1095 |
|
1096 # relations cache handling ################################################# |
|
1097 |
|
1098 update_rel_cache_add = cnx_meth('update_rel_cache_add') |
|
1099 update_rel_cache_del = cnx_meth('update_rel_cache_del') |
1095 |
1100 |
1096 # resource accessors ###################################################### |
1101 # resource accessors ###################################################### |
1097 |
1102 |
1098 system_sql = cnx_meth('system_sql') |
1103 system_sql = cnx_meth('system_sql') |
1099 deleted_in_transaction = cnx_meth('deleted_in_transaction') |
1104 deleted_in_transaction = cnx_meth('deleted_in_transaction') |