diff --git a/product/ERP5Subversion/.cvsignore b/product/ERP5Subversion/.cvsignore new file mode 100755 index 0000000000000000000000000000000000000000..d0743b7da685f380ff337cb0f7bf7f6fd7956ec3 --- /dev/null +++ b/product/ERP5Subversion/.cvsignore @@ -0,0 +1,2 @@ +.AppleDouble +*.pyc diff --git a/product/ERP5Subversion/Constraint/.cvsignore b/product/ERP5Subversion/Constraint/.cvsignore new file mode 100755 index 0000000000000000000000000000000000000000..d0743b7da685f380ff337cb0f7bf7f6fd7956ec3 --- /dev/null +++ b/product/ERP5Subversion/Constraint/.cvsignore @@ -0,0 +1,2 @@ +.AppleDouble +*.pyc diff --git a/product/ERP5Subversion/Constraint/__init__.py b/product/ERP5Subversion/Constraint/__init__.py new file mode 100755 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/product/ERP5Subversion/Document/.cvsignore b/product/ERP5Subversion/Document/.cvsignore new file mode 100755 index 0000000000000000000000000000000000000000..d0743b7da685f380ff337cb0f7bf7f6fd7956ec3 --- /dev/null +++ b/product/ERP5Subversion/Document/.cvsignore @@ -0,0 +1,2 @@ +.AppleDouble +*.pyc diff --git a/product/ERP5Subversion/Document/__init__.py b/product/ERP5Subversion/Document/__init__.py new file mode 100755 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/product/ERP5Subversion/Interface/.cvsignore b/product/ERP5Subversion/Interface/.cvsignore new file mode 100755 index 0000000000000000000000000000000000000000..d0743b7da685f380ff337cb0f7bf7f6fd7956ec3 --- /dev/null +++ b/product/ERP5Subversion/Interface/.cvsignore @@ -0,0 +1,2 @@ +.AppleDouble +*.pyc diff --git a/product/ERP5Subversion/Interface/__init__.py b/product/ERP5Subversion/Interface/__init__.py new file mode 100755 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/product/ERP5Subversion/Permissions.py b/product/ERP5Subversion/Permissions.py new file mode 100755 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/product/ERP5Subversion/PropertySheet/.cvsignore b/product/ERP5Subversion/PropertySheet/.cvsignore new file mode 100755 index 0000000000000000000000000000000000000000..d0743b7da685f380ff337cb0f7bf7f6fd7956ec3 --- /dev/null +++ b/product/ERP5Subversion/PropertySheet/.cvsignore @@ -0,0 +1,2 @@ +.AppleDouble +*.pyc diff --git a/product/ERP5Subversion/PropertySheet/__init__.py b/product/ERP5Subversion/PropertySheet/__init__.py new file mode 100755 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/product/ERP5Subversion/SubversionClient.py b/product/ERP5Subversion/SubversionClient.py new file mode 100755 index 0000000000000000000000000000000000000000..4b1d08555923d13eeb399a93868bcb9757237556 --- /dev/null +++ b/product/ERP5Subversion/SubversionClient.py @@ -0,0 +1,206 @@ +############################################################################## +# +# Copyright (c) 2005 Nexedi SARL and Contributors. All Rights Reserved. +# Yoshinori Okuji <yo@nexedi.com> +# +# WARNING: This program as such is intended to be used by professional +# programmers who take the whole responsability of assessing all potential +# consequences resulting from its eventual inadequacies and bugs +# End users who are looking for a ready-to-use solution with commercial +# garantees and support are strongly adviced to contract a Free Software +# Service Company +# +# This program is Free Software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. +# +############################################################################## + +from Acquisition import Implicit +import time +from Products.ERP5Type.Utils import convertToUpperCase +from MethodObject import Method +from Globals import InitializeClass +from AccessControl import ClassSecurityInfo + +try: + import pysvn + + class SubversionError(Exception): + """The base exception class for the Subversion interface. + """ + pass + + class SubversionInstallationError(SubversionError): + """Raised when an installation is broken. + """ + pass + + class SubversionTimeoutError(SubversionError): + """Raised when a Subversion transaction is too long. + """ + pass + + class SubversionLoginError(SubversionError): + """Raised when an authentication is required. + """ + def __init__(self, realm = None): + self._realm = realm + + def getRealm(self): + return self._realm + + class SubversionSSLTrustError(SubversionError): + """Raised when a SSL certificate is not trusted. + """ + def __init__(self, trust_dict = None): + self._trust_dict = trust_dict + + def getTrustDict(self): + return self._trust_dict + + + class Callback: + """The base class for callback functions. + """ + def __init__(self, client): + self.client = client + + def __call__(self, *args): + pass + + class CancelCallback(Callback): + def __call__(self): + current_time = time.time() + if current_time - self.client.creation_time > self.client.getTimeout(): + raise SubversionTimeoutError, 'too long transaction' + #return True + return False + + class GetLogMessageCallback(Callback): + def __call__(self): + message = self.client.getLogMessage() + if message: + return True, message + return False, '' + + class GetLoginCallback(Callback): + def __call__(self, realm, username, may_save): + user, password = self.client.getLogin(realm) + if user is None: + raise SubversionLoginError(realm) + #return False, '', '', False + return True, user, password, False + + class NotifyCallback(Callback): + def __call__(self, event_dict): + # FIXME: should accumulate information for the user + pass + + class SSLServerTrustPromptCallback(Callback): + def __call__(self, trust_dict): + trust, permanent = self.client.trustSSLServer(trust_dict) + if not trust: + raise SubversionSSLTrustError(trust_dict) + #return False, 0, False + # XXX SSL server certificate failure bits are not defined in pysvn. + # 0x8 means that the CA is unknown. + return True, 0x8, permanent + + # Wrap objects defined in pysvn so that skins have access to attributes in the ERP5 way. + class Getter(Method): + def __init__(self, key): + self._key = key + + def __call__(self, instance): + value = getattr(instance._obj, self._key) + if type(value) == type(u''): + value = value.encode('utf-8') + elif isinstance(value, pysvn.Entry): + value = Entry(value) + elif isinstance(value, pysvn.Revision): + value = Revision(value) + return value + + def initializeAccessors(klass): + klass.security = ClassSecurityInfo() + for attr in klass.attribute_list: + name = 'get' + convertToUpperCase(attr) + setattr(klass, name, Getter(attr)) + klass.security.declarePublic(name) + InitializeClass(Status) + + class ObjectWrapper(Implicit): + attribute_list = () + + def __init__(self, obj): + self._obj = obj + + class Status(ObjectWrapper): + attribute_list = ('path', 'entry', 'is_versioned', 'is_locked', 'is_copied', 'is_switched', + 'prop_status', 'text_status', 'repos_prop_status', 'repos_text_status') + initializeAccessors(Status) + + class Entry(ObjectWrapper): + attribute_list = ('checksum', 'commit_author', 'commit_revision', 'commit_time', + 'conflict_new', 'conflict_old', 'conflict_work', 'copy_from_revision', + 'copy_from_url', 'is_absent', 'is_copied', 'is_deleted', 'is_valid', + 'kind', 'name', 'properties_time', 'property_reject_file', 'repos', + 'revision', 'schedule', 'text_time', 'url', 'uuid') + + class Revision(ObjectWrapper): + attribute_list = ('kind', 'date', 'number') + initializeAccessors(Revision) + + + class SubversionClient(Implicit): + """This class wraps pysvn's Client class. + """ + log_message = None + timeout = 60 * 5 + + def __init__(self, **kw): + self.client = pysvn.Client() + self.client.set_auth_cache(0) + self.client.callback_cancel = CancelCallback(self) + self.client.callback_get_log_message = GetLogMessageCallback(self) + self.client.callback_get_login = GetLoginCallback(self) + self.client.callback_notify = NotifyCallback(self) + self.client.callback_ssl_server_trust_prompt = SSLServerTrustPromptCallback(self) + self.creation_time = time.time() + self.__dict__.update(kw) + + def getLogMessage(self): + return self.log_message + + def getTimeout(self): + return self.timeout + + def getLogin(self, realm): + return self.aq_parent._getLogin(realm) + + def trustSSLServer(self, trust_dict): + return self.aq_parent._trustSSLServer(trust_dict) + + def status(self, path, **kw): + # Since plain Python classes are not convenient in Zope, convert the objects. + return [Status(x) for x in self.client.status(path, **kw)] + + def newSubversionClient(container, **kw): + return SubversionClient(**kw).__of__(container) + +except ImportError: + LOG('SubversionTool', WARNING, + 'could not import pysvn; until pysvn is installed properly, this tool will not function.') + def newSubversionClient(container, **kw): + raise SubversionInstallationError, 'pysvn is not installed' diff --git a/product/ERP5Subversion/Tool/.cvsignore b/product/ERP5Subversion/Tool/.cvsignore new file mode 100755 index 0000000000000000000000000000000000000000..d0743b7da685f380ff337cb0f7bf7f6fd7956ec3 --- /dev/null +++ b/product/ERP5Subversion/Tool/.cvsignore @@ -0,0 +1,2 @@ +.AppleDouble +*.pyc diff --git a/product/ERP5Subversion/Tool/SubversionTool.py b/product/ERP5Subversion/Tool/SubversionTool.py new file mode 100755 index 0000000000000000000000000000000000000000..1246906f4cdc1683ee2df6124c3835c9c399dcaa --- /dev/null +++ b/product/ERP5Subversion/Tool/SubversionTool.py @@ -0,0 +1,236 @@ +############################################################################## +# +# Copyright (c) 2005 Nexedi SARL and Contributors. All Rights Reserved. +# Yoshinori Okuji <yo@nexedi.com> +# +# WARNING: This program as such is intended to be used by professional +# programmers who take the whole responsability of assessing all potential +# consequences resulting from its eventual inadequacies and bugs +# End users who are looking for a ready-to-use solution with commercial +# garantees and support are strongly adviced to contract a Free Software +# Service Company +# +# This program is Free Software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. +# +############################################################################## + +from Products.CMFCore.utils import UniqueObject +from AccessControl import ClassSecurityInfo +from Globals import InitializeClass, DTMLFile +from Products.ERP5Type.Document.Folder import Folder +from Products.ERP5Type import Permissions +from Products.ERP5Subversion import _dtmldir +from zLOG import LOG, WARNING, INFO +from Products.ERP5Subversion.SubversionClient import newSubversionClient +import os +from DateTime import DateTime +from base64 import b64encode, b64decode +from cPickle import dumps, loads +from App.config import getConfiguration +from zExceptions import Unauthorized + +class SubversionTool(UniqueObject, Folder): + """The SubversionTool provides a Subversion interface to ERP5. + """ + id = 'portal_subversion' + meta_type = 'ERP5 Subversion Tool' + portal_type = 'Subversion Tool' + allowed_types = () + + login_cookie_name = 'erp5_subversion_login' + ssl_trust_cookie_name = 'erp5_subversion_ssl_trust' + top_working_path = os.path.join(getConfiguration().instancehome, 'svn') + + # Declarative Security + security = ClassSecurityInfo() + + # + # ZMI methods + # + manage_options = ( ( { 'label' : 'Overview' + , 'action' : 'manage_overview' + } + , + ) + + Folder.manage_options + ) + + security.declareProtected( Permissions.ManagePortal, 'manage_overview' ) + manage_overview = DTMLFile( 'explainSubversionTool', _dtmldir ) + + # Filter content (ZMI)) + def __init__(self): + return Folder.__init__(self, SubversionTool.id) + + # Filter content (ZMI)) + def filtered_meta_types(self, user=None): + # Filters the list of available meta types. + all = SubversionTool.inheritedAttribute('filtered_meta_types')(self) + meta_types = [] + for meta_type in self.all_meta_types(): + if meta_type['name'] in self.allowed_types: + meta_types.append(meta_type) + return meta_types + + def getTopWorkingPath(self): + return self.top_working_path + + def _getWorkingPath(self, path): + if path[0] != '/': + path = os.path.join(self.top_working_path, path) + path = os.path.abspath(path) + if not path.startswith(self.top_working_path): + raise Unauthorized, 'unauthorized access to path %s' % path + return path + + def _encodeLogin(self, realm, user, password): + # Encode login information. + return b64encode(dumps((realm, user, password))) + + def _decodeLogin(self, login): + # Decode login information. + return loads(b64decode(login)) + + def setLogin(self, realm, user, password): + """Set login information. + """ + # Get existing login information. Filter out old information. + login_list = [] + request = self.REQUEST + cookie = request.get(self.login_cookie_name) + if cookie: + for login in cookie.split(','): + if self._decodeLogin(login)[0] != realm: + login_list.append(login) + # Set the cookie. + response = request.RESPONSE + login_list.append(self._encodeLogin(realm, user, password)) + value = ','.join(login_list) + expires = (DateTime() + 1).toZone('GMT').rfc822() + response.setCookie(self.login_cookie_name, value, path = '/', expires = expires) + + def _getLogin(self, target_realm): + request = self.REQUEST + cookie = request.get(self.login_cookie_name) + if cookie: + for login in cookie.split(','): + realm, user, password = self._decodeLogin(login) + if target_realm == realm: + return user, password + return None, None + + def _encodeSSLTrust(self, trust_dict, permanent=False): + # Encode login information. + key_list = trust_dict.keys() + key_list.sort() + trust_item_list = tuple([(key, trust_dict[key]) for key in key_list]) + return b64encode(dumps((trust_item_list, permanent))) + + def _decodeSSLTrust(self, trust): + # Decode login information. + trust_item_list, permanent = loads(b64decode(login)) + return dict(trust_item_list), permanent + + security.declareProtected(Permissions.ManagePortal, 'acceptSSLServer') + def acceptSSLServer(self, trust_dict, permanent=False): + """Accept a SSL server. + """ + # Get existing trust information. + trust_list = [] + request = self.REQUEST + cookie = request.get(self.ssl_trust_cookie_name) + if cookie: + trust.append(cookie) + # Set the cookie. + response = request.RESPONSE + trust_list.append(self._encodeSSLTrust(trust_dict, permanent)) + value = ','.join(trust_list) + expires = (DateTime() + 1).toZone('GMT').rfc822() + response.setCookie(self.ssl_trust_cookie_name, value, path = '/', expires = expires) + + def _trustSSLServer(self, target_trust_dict): + request = self.REQUEST + cookie = request.get(self.ssl_trust_cookie_name) + if cookie: + for trust in cookie.split(','): + trust_dict, permanent = self._decodeSSLTrust(trust) + for key in target_trust_dict.keys(): + if target_trust_dict[key] != trust_dict.get(key): + continue + else: + return True, permanent + return False, False + + def _getClient(self, **kw): + # Get the svn client object. + return newSubversionClient(self, **kw) + + security.declareProtected('Import/Export objects', 'update') + def update(self, path): + """Update a working copy. + """ + client = self._getClient() + return client.update(self._getWorkingPath(path)) + + security.declareProtected('Import/Export objects', 'add') + def add(self, path): + """Add a file or a directory. + """ + client = self._getClient() + return client.add(self._getWorkingPath(path)) + + security.declareProtected('Import/Export objects', 'remove') + def remove(self, path): + """Remove a file or a directory. + """ + client = self._getClient() + return client.remove(self._getWorkingPath(path)) + + security.declareProtected('Import/Export objects', 'move') + def move(self, src, dest): + """Move/Rename a file or a directory. + """ + client = self._getClient() + return client.move(src, dest) + + security.declareProtected('Import/Export objects', 'diff') + def diff(self, path): + """Make a diff for a file or a directory. + """ + client = self._getClient() + return client.diff(self._getWorkingPath(path)) + + security.declareProtected('Import/Export objects', 'revert') + def revert(self, path): + """Revert local changes in a file or a directory. + """ + client = self._getClient() + return client.revert(self._getWorkingPath(path)) + + security.declareProtected('Import/Export objects', 'checkin') + def checkin(self, path, log_message = None): + """Commit local changes. + """ + client = self._getClient(log_message = log_message) + return client.checkin(self._getWorkingPath(path)) + + security.declareProtected('Import/Export objects', 'status') + def status(self, path, **kw): + """Get status. + """ + client = self._getClient() + return client.status(self._getWorkingPath(path), **kw) + +InitializeClass(SubversionTool) diff --git a/product/ERP5Subversion/Tool/__init__.py b/product/ERP5Subversion/Tool/__init__.py new file mode 100755 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/product/ERP5Subversion/__init__.py b/product/ERP5Subversion/__init__.py new file mode 100755 index 0000000000000000000000000000000000000000..ab28f9b0f62699c49c6c783d511d4bc3b0415d51 --- /dev/null +++ b/product/ERP5Subversion/__init__.py @@ -0,0 +1,55 @@ +############################################################################## +# +# Copyright (c) 2005 Nexedi SARL and Contributors. All Rights Reserved. +# Yoshinori Okuji <yo@nexedi.com> +# +# WARNING: This program as such is intended to be used by professional +# programmers who take the whole responsability of assessing all potential +# consequences resulting from its eventual inadequacies and bugs +# End users who are looking for a ready-to-use solution with commercial +# garantees and support are strongly adviced to contract a Free Software +# Service Company +# +# This program is Free Software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. +# +############################################################################## +""" + ERP5 Free Software ERP +""" + +# Update ERP5 Globals +from Products.ERP5Type.Utils import initializeProduct, updateGlobals +import sys, Permissions +this_module = sys.modules[ __name__ ] +document_classes = updateGlobals( this_module, globals(), permissions_module = Permissions) + +from Tool import SubversionTool + +# Define object classes and tools +object_classes = () +portal_tools = (SubversionTool.SubversionTool,) +content_classes = () +content_constructors = () + +# Finish installation +def initialize( context ): + import Document + initializeProduct(context, this_module, globals(), + document_module = Document, + document_classes = document_classes, + object_classes = object_classes, + portal_tools = portal_tools, + content_constructors = content_constructors, + content_classes = content_classes) diff --git a/product/ERP5Subversion/dtml/explainSubversionTool.dtml b/product/ERP5Subversion/dtml/explainSubversionTool.dtml new file mode 100755 index 0000000000000000000000000000000000000000..470a8e6d839878716cb64b61dcc0926945a0a188 --- /dev/null +++ b/product/ERP5Subversion/dtml/explainSubversionTool.dtml @@ -0,0 +1,6 @@ +<dtml-var manage_page_header> +<dtml-var manage_tabs> + +<p>SubversionTool provides a Subversion interface for ERP5.</p> + +<dtml-var manage_page_footer> diff --git a/product/ERP5Subversion/skins/.cvsignore b/product/ERP5Subversion/skins/.cvsignore new file mode 100755 index 0000000000000000000000000000000000000000..d0743b7da685f380ff337cb0f7bf7f6fd7956ec3 --- /dev/null +++ b/product/ERP5Subversion/skins/.cvsignore @@ -0,0 +1,2 @@ +.AppleDouble +*.pyc diff --git a/product/ERP5Subversion/skins/__init__.py b/product/ERP5Subversion/skins/__init__.py new file mode 100755 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/product/ERP5Subversion/tool.png b/product/ERP5Subversion/tool.png new file mode 100755 index 0000000000000000000000000000000000000000..ab2c1f5a76622765a48e8f9cc516398b9a3a4213 Binary files /dev/null and b/product/ERP5Subversion/tool.png differ