diff --git a/CHANGES.rst b/CHANGES.rst
index 7c11552dfb..40d8c09967 100644
--- a/CHANGES.rst
+++ b/CHANGES.rst
@@ -139,8 +139,6 @@ Restructuring
- Drop `OFS.History` functionality.
-- Removed ZMI export/import feature.
-
- Drop ZopeUndo dependency and move undo management to the control panel.
- Simplify ZMI control panel and globally available management screens.
diff --git a/src/OFS/ObjectManager.py b/src/OFS/ObjectManager.py
index d140814581..820e4761c8 100644
--- a/src/OFS/ObjectManager.py
+++ b/src/OFS/ObjectManager.py
@@ -14,6 +14,7 @@
"""
from cgi import escape
+from cStringIO import StringIO
from logging import getLogger
import copy
import fnmatch
@@ -31,6 +32,7 @@
from AccessControl.Permissions import access_contents_information
from AccessControl.Permissions import delete_objects
from AccessControl.Permissions import ftp_access
+from AccessControl.Permissions import import_export_objects
from AccessControl import getSecurityManager
from AccessControl.ZopeSecurityPolicy import getRoles
from Acquisition import aq_base, aq_acquire, aq_parent
@@ -59,6 +61,10 @@
from OFS.event import ObjectWillBeRemovedEvent
from OFS.Lockable import LockableItem
from OFS.subscribers import compatibilityCall
+from OFS.XMLExportImport import importXML
+from OFS.XMLExportImport import exportXML
+from OFS.XMLExportImport import magic
+
import collections
if bbb.HAS_ZSERVER:
@@ -80,8 +86,10 @@
# the name BadRequestException is relied upon by 3rd-party code
BadRequestException = BadRequest
-bad_id = re.compile(r'[^a-zA-Z0-9-_~,.$\(\)# @]').search
+customImporters={magic: importXML,
+ }
+bad_id=re.compile(r'[^a-zA-Z0-9-_~,.$\(\)# @]').search
def checkValidId(self, id, allow_dup=0):
# If allow_dup is false, an error will be raised if an object
@@ -569,6 +577,92 @@ def tpValues(self):
r.append(o)
return r
+ security.declareProtected(import_export_objects, 'manage_exportObject')
+ def manage_exportObject(self, id='', download=None, toxml=None,
+ RESPONSE=None,REQUEST=None):
+ """Exports an object to a file and returns that file."""
+ if not id:
+ # can't use getId() here (breaks on "old" exported objects)
+ id=self.id
+ if hasattr(id, 'im_func'): id=id()
+ ob=self
+ else: ob=self._getOb(id)
+
+ suffix=toxml and 'xml' or 'zexp'
+
+ if download:
+ f=StringIO()
+ if toxml:
+ exportXML(ob._p_jar, ob._p_oid, f)
+ else:
+ ob._p_jar.exportFile(ob._p_oid, f)
+ if RESPONSE is not None:
+ RESPONSE.setHeader('Content-type','application/data')
+ RESPONSE.setHeader('Content-Disposition',
+ 'inline;filename=%s.%s' % (id, suffix))
+ return f.getvalue()
+
+ cfg = getConfiguration()
+ f = os.path.join(cfg.clienthome, '%s.%s' % (id, suffix))
+ if toxml:
+ exportXML(ob._p_jar, ob._p_oid, f)
+ else:
+ ob._p_jar.exportFile(ob._p_oid, f)
+
+ if REQUEST is not None:
+ return self.manage_main(self, REQUEST,
+ manage_tabs_message=
+ '%s successfully exported to %s' % (id,f),
+ title = 'Object exported')
+
+
+ security.declareProtected(import_export_objects, 'manage_importExportForm')
+ manage_importExportForm=DTMLFile('dtml/importExport',globals())
+
+ security.declareProtected(import_export_objects, 'manage_importObject')
+ def manage_importObject(self, file, REQUEST=None, set_owner=1):
+ """Import an object from a file"""
+ dirname, file=os.path.split(file)
+ if dirname:
+ raise BadRequest, 'Invalid file name %s' % escape(file)
+
+ for impath in self._getImportPaths():
+ filepath = os.path.join(impath, 'import', file)
+ if os.path.exists(filepath):
+ break
+ else:
+ raise BadRequest, 'File does not exist: %s' % escape(file)
+
+ self._importObjectFromFile(filepath, verify=not not REQUEST,
+ set_owner=set_owner)
+
+ if REQUEST is not None:
+ return self.manage_main(
+ self, REQUEST,
+ manage_tabs_message='%s successfully imported' % id,
+ title='Object imported',
+ update_menu=1)
+
+ def _importObjectFromFile(self, filepath, verify=1, set_owner=1):
+ # locate a valid connection
+ connection=self._p_jar
+ obj=self
+
+ while connection is None:
+ obj=obj.aq_parent
+ connection=obj._p_jar
+ ob=connection.importFile(
+ filepath, customImporters=customImporters)
+ if verify: self._verifyObjectPaste(ob, validate_src=0)
+ id=ob.id
+ if hasattr(id, 'im_func'): id=id()
+ self._setObject(id, ob, set_owner=set_owner)
+
+ # try to make ownership implicit if possible in the context
+ # that the object was imported into.
+ ob=self._getOb(id)
+ ob.manage_changeOwnershipType(explicit=0)
+
def _getImportPaths(self):
cfg = getConfiguration()
paths = []
diff --git a/src/OFS/XMLExportImport.py b/src/OFS/XMLExportImport.py
new file mode 100644
index 0000000000..e8addc5e33
--- /dev/null
+++ b/src/OFS/XMLExportImport.py
@@ -0,0 +1,124 @@
+##############################################################################
+#
+# Copyright (c) 2002 Zope Foundation and Contributors.
+#
+# This software is subject to the provisions of the Zope Public License,
+# Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution.
+# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
+# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
+# FOR A PARTICULAR PURPOSE
+#
+##############################################################################
+from base64 import encodestring
+from cStringIO import StringIO
+from ZODB.serialize import referencesf
+from ZODB.ExportImport import TemporaryFile, export_end_marker
+from ZODB.utils import p64
+from ZODB.utils import u64
+from Shared.DC.xml import ppml
+
+
+magic='\n%s \n' % (id, aka, p)
+ return String
+
+def exportXML(jar, oid, file=None):
+
+ if file is None: file=TemporaryFile()
+ elif type(file) is str: file=open(file,'w+b')
+ write=file.write
+ write('\012\012')
+ ref=referencesf
+ oids=[oid]
+ done_oids={}
+ done=done_oids.has_key
+ load=jar._storage.load
+ while oids:
+ oid=oids[0]
+ del oids[0]
+ if done(oid): continue
+ done_oids[oid]=1
+ try:
+ try:
+ p, serial = load(oid)
+ except TypeError:
+ # Some places inside the ZODB 3.9 still want a version
+ # argument, for example TmpStore from Connection.py
+ p, serial = load(oid, None)
+ except:
+ pass # Ick, a broken reference
+ else:
+ ref(p, oids)
+ write(XMLrecord(oid,len(p),p))
+ write('\n')
+ return file
+
+class zopedata:
+ def __init__(self, parser, tag, attrs):
+ self.file=parser.file
+ write=self.file.write
+ write('ZEXP')
+
+ def append(self, data):
+ file=self.file
+ write=file.write
+ pos=file.tell()
+ file.seek(pos)
+ write(data)
+
+def start_zopedata(parser, tag, data):
+ return zopedata(parser, tag, data)
+
+def save_zopedata(parser, tag, data):
+ file=parser.file
+ write=file.write
+ pos=file.tell()
+ file.seek(pos)
+ write(export_end_marker)
+
+def save_record(parser, tag, data):
+ file=parser.file
+ write=file.write
+ pos=file.tell()
+ file.seek(pos)
+ a=data[1]
+ if a.has_key('id'): oid=a['id']
+ oid=p64(int(oid))
+ v=''
+ for x in data[2:]:
+ v=v+x
+ l=p64(len(v))
+ v=oid+l+v
+ return v
+
+def importXML(jar, file, clue=''):
+ import xml.parsers.expat
+ if type(file) is str:
+ file=open(file, 'rb')
+ outfile=TemporaryFile()
+ data=file.read()
+ F=ppml.xmlPickler()
+ F.end_handlers['record'] = save_record
+ F.end_handlers['ZopeData'] = save_zopedata
+ F.start_handlers['ZopeData'] = start_zopedata
+ F.binary=1
+ F.file=outfile
+ p=xml.parsers.expat.ParserCreate()
+ p.CharacterDataHandler=F.handle_data
+ p.StartElementHandler=F.unknown_starttag
+ p.EndElementHandler=F.unknown_endtag
+ r=p.Parse(data)
+ outfile.seek(0)
+ return jar.importFile(outfile,clue)
diff --git a/src/OFS/dtml/importExport.dtml b/src/OFS/dtml/importExport.dtml
new file mode 100644
index 0000000000..156793400a
--- /dev/null
+++ b/src/OFS/dtml/importExport.dtml
@@ -0,0 +1,141 @@
+
+
+
+
+You can export Zope objects to a file in order to transfer
+them to a different Zope installation. You can either choose
+to download the export file to your local machine, or save it
+in the "var" directory of your Zope installation
+on the server.
+
+
+Note:
+Zope can export/import objects in two different formats: a binary format (called
+ZEXP) and as XML. The ZEXP format is the officially supported export/import
+format for moving data between identical Zope installations (it is not a migration tool).
+
+
+
+
+
+
+
+You may import Zope objects which have been previously
+exported to a file, by placing the file in the "import"
+directory of your Zope installation on the server. You should create
+the "import" directory in the root of your Zope installation
+if it does not yet exist.
+
+
+
+Note that by default, you will become the owner of the objects
+that you are importing. If you wish the imported objects to retain
+their existing ownership information, select "retain existing
+ownership information".
+