1052 lines
41 KiB
Python
1052 lines
41 KiB
Python
# -*- coding: UTF-8 -*-
|
|
|
|
|
|
# Expression
|
|
# By: Frederic Peters <fpeters@entrouvert.com>
|
|
# Emmanuel Raviart <eraviart@entrouvert.com>
|
|
#
|
|
# Copyright (C) 2004 Entr'ouvert, Frederic Peters & Emmanuel Raviart
|
|
#
|
|
# 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.
|
|
|
|
|
|
"""Data Holders Module."""
|
|
|
|
|
|
import errno
|
|
import os
|
|
import time
|
|
|
|
import libxml2
|
|
|
|
import documents
|
|
import environs
|
|
import faults
|
|
import filesystems
|
|
import locations
|
|
import logs
|
|
import modules
|
|
import namespaces
|
|
import stations
|
|
import xpaths
|
|
|
|
|
|
holderClassesByMimeType = {}
|
|
mimeTypes = {
|
|
".css": "text/css",
|
|
".csv": "text/x-csv",
|
|
".dbxml": "application/x-dbxml",
|
|
".der": "application/x-x509-ca-cert",
|
|
".foaf": "application/rdf+xml",
|
|
".gif": "image/gif",
|
|
".htc": "text/x-component",
|
|
".htm": "text/html",
|
|
".html": "text/html",
|
|
".jpeg": "image/jpeg",
|
|
".jpg": "image/jpeg",
|
|
".js": "text/javascript",
|
|
".pdf": "application/pdf",
|
|
".pem": "application/x-x509-ca-cert",
|
|
".png": "image/png",
|
|
".py": "text/x-python-src",
|
|
".rst": "x-text/restructured",
|
|
".stc": "application/vnd.sun.xml.calc.template",
|
|
".std": "application/vnd.sun.xml.draw.template",
|
|
".sti": "application/vnd.sun.xml.impress.template",
|
|
".stw": "application/vnd.sun.xml.writer.template",
|
|
".svg": "image/svg+xml",
|
|
".sxc": "application/vnd.sun.xml.calc",
|
|
".sxd": "application/vnd.sun.xml.draw",
|
|
".sxg": "application/vnd.sun.xml.writer.global",
|
|
".sxi": "application/vnd.sun.xml.impress",
|
|
".sxm": "application/vnd.sun.xml.math",
|
|
".sxw": "application/vnd.sun.xml.writer",
|
|
".txt": "text/plain",
|
|
".xhtml": "application/xhtml+xml",
|
|
".xml": "text/xml",
|
|
".xsd": "text/xml",
|
|
".xsl": "text/xml",
|
|
".zip": "application/zip",
|
|
}
|
|
|
|
|
|
class DataHolder(stations.AbstractStation):
|
|
_configDoc = None
|
|
_containedFileSystem = None
|
|
_data = None
|
|
_dataHolderInited = False
|
|
_metadata = None
|
|
_xslt = None
|
|
defaultFileNameExtension = None
|
|
mimeType = None
|
|
mimeTypes = None
|
|
pathFragment = None
|
|
|
|
def __del__(self):
|
|
if self._configDoc not in (None, "none"):
|
|
self._configDoc.freeDoc()
|
|
del self._configDoc
|
|
|
|
def __init__(self, pathFragment = None, previous = None, parent = None,
|
|
uriPathFragment = None, uriScheme = None, uriAuthority = None,
|
|
isRemote = False, remoteUri = None, mimeType = None, isRootElder = False,
|
|
containedFileSystem = None):
|
|
if self._dataHolderInited:
|
|
return
|
|
self._dataHolderInited = True
|
|
|
|
super(DataHolder, self).__init__(
|
|
previous = previous, parent = parent, uriPathFragment = uriPathFragment,
|
|
uriScheme = uriScheme, uriAuthority = uriAuthority, isRemote = isRemote,
|
|
remoteUri = remoteUri)
|
|
if pathFragment:
|
|
# When self is a root elder, pathFragment must be an absolute path.
|
|
self.pathFragment = pathFragment
|
|
if mimeType is not None:
|
|
self.mimeType = mimeType
|
|
if isRootElder:
|
|
self.isRootElder = True
|
|
if containedFileSystem is not None:
|
|
self._containedFileSystem = containedFileSystem
|
|
|
|
def __new__(cls, *arguments, **keywords):
|
|
self = stations.AbstractStation.__new__(cls, *arguments, **keywords)
|
|
self.__init__(*arguments, **keywords)
|
|
if self.mimeType is None:
|
|
fileSystem = self.fileSystem
|
|
fileSystemPath = self.getFileSystemPath()
|
|
if fileSystem.exists(fileSystemPath) and fileSystem.isdir(fileSystemPath):
|
|
self.mimeType = "inode/directory"
|
|
if self.mimeType is None:
|
|
logs.debug('Missing MIME type for "%s" using StaticDataHolder.' % self)
|
|
realClass = StaticDataHolder
|
|
else:
|
|
logs.debug("MIME: %s" % str(self.mimeType))
|
|
realClass = self.getConfigPythonClass(
|
|
"yep:pythonClass", default = None, ignoreGeneralValue = True)
|
|
if realClass is None:
|
|
if self.mimeType in holderClassesByMimeType:
|
|
realClass = holderClassesByMimeType[self.mimeType]
|
|
else:
|
|
logs.debug('Unknown MIME type "%s" using StaticDataHolder.' % self.mimeType)
|
|
realClass = StaticDataHolder
|
|
if realClass != self.__class__ and issubclass(realClass, self.__class__):
|
|
self.__class__ = realClass
|
|
return self
|
|
|
|
def doHttpDelete(self):
|
|
content = environs.getVar("submission").readFile()
|
|
if content is not None:
|
|
logs.debug(content)
|
|
fileSystemPath = self.getFileSystemPath()
|
|
try:
|
|
self.fileSystem.remove(fileSystemPath)
|
|
except OSError, error:
|
|
if error.errno != errno.EPERM:
|
|
raise
|
|
return environs.getVar("httpRequestHandler").outputErrorAccessForbidden(
|
|
self.getUriAbsolutePath())
|
|
environs.getVar("httpRequestHandler").outputSuccessNoContent()
|
|
|
|
def doHttpDelete1(self, multistatus):
|
|
fileSystemPath = self.getFileSystemPath()
|
|
try:
|
|
self.fileSystem.remove(fileSystemPath)
|
|
except OSError, error:
|
|
if error.errno != errno.EPERM:
|
|
raise
|
|
multistatusNode = multistatus.node
|
|
responseNode = multistatusNode.newTextChild(None, "response", None)
|
|
# The href URI is quoted, because Nautilus requires that the returned href is the
|
|
# same as the URL in the PROPFIND command. Otherwise it can not open "untitled
|
|
# folder", because it expects "untitled%20folder".
|
|
responseNode.newTextChild(None, "href", self.getAbsoluteUri(quote = True))
|
|
responseNode.newTextChild(None, "status", "HTTP/1.1 403 Access Forbidden")
|
|
return False
|
|
return True
|
|
|
|
def doHttpPut(self):
|
|
# FIXME: To complete.
|
|
# See RFC 2616 <http://www.ietf.org/rfc/rfc2616.txt>
|
|
# and the full thread at
|
|
# <http://www.mail-archive.com/www-talk@w3.org/msg00699.html>
|
|
# to know how to handle HTTP PUT correctly.
|
|
|
|
httpRequestHandler = environs.getVar("httpRequestHandler")
|
|
# FIXME: We should check whether the save can be done before sending
|
|
# a "continue" response.
|
|
expect = httpRequestHandler.headers.get("Expect", "")
|
|
if expect.lower() == "100-continue":
|
|
httpRequestHandler.outputInformationContinue()
|
|
submission = environs.getVar("submission")
|
|
isCreated = self.saveDataFile(submission.file, submission.length)
|
|
if isCreated:
|
|
httpRequestHandler.outputSuccessCreated(self.getUriAbsolutePath())
|
|
else:
|
|
httpRequestHandler.outputSuccessNoContent()
|
|
|
|
def getAbsolutePath(self):
|
|
parent = self.getParent()
|
|
if parent is None:
|
|
absolutePath = "/"
|
|
else:
|
|
absolutePath = parent.getAbsolutePath()
|
|
if self.pathFragment:
|
|
absolutePath = os.path.normpath(os.path.join(absolutePath, self.pathFragment))
|
|
return absolutePath
|
|
|
|
def getAllConfigElementNodesAndOwnerCouples(self, xpath):
|
|
relevantNodesAndOwnerCouples = super(
|
|
DataHolder, self).getAllConfigElementNodesAndOwnerCouples(xpath)
|
|
configDoc = self.getConfigDoc()
|
|
elementNodes = xpaths.evaluateXpath("yep:element", contextNode = configDoc)
|
|
for elementNode in elementNodes:
|
|
nodes = xpaths.evaluateXpath(xpath, contextNode = elementNode)
|
|
if not nodes:
|
|
continue
|
|
nameNodes = xpaths.evaluateXpath("@name", contextNode = elementNode)
|
|
if not nameNodes:
|
|
continue
|
|
name = nameNodes[0].content
|
|
namespaceUriNodes = xpaths.evaluateXpath("@namespace", contextNode = elementNode)
|
|
if not namespaceUriNodes:
|
|
continue
|
|
namespaceUri = namespaceUriNodes[0].content
|
|
key = "%s / %s" % (namespaceUri, name)
|
|
relevantNodesAndOwnerCouples[key] = (nodes, self)
|
|
return relevantNodesAndOwnerCouples
|
|
|
|
def getConfigDataHolderNodesAndOwner(self, mimeType, xpath, ignoreGeneralValue = False):
|
|
configDoc = self.getConfigDoc()
|
|
dataHolderNodes = xpaths.evaluateXpath(
|
|
"yep:inode[@mimeType = '%s']" % mimeType.replace("'", "'"),
|
|
contextNode = configDoc)
|
|
if dataHolderNodes:
|
|
dataHolderNode = dataHolderNodes[0]
|
|
nodes = xpaths.evaluateXpath(xpath, contextNode = dataHolderNode)
|
|
if nodes:
|
|
return nodes, self
|
|
if not ignoreGeneralValue:
|
|
nodes = xpaths.evaluateXpath(xpath, contextNode = configDoc)
|
|
if nodes:
|
|
return nodes, self
|
|
return super(DataHolder, self).getConfigDataHolderNodesAndOwner(
|
|
mimeType, xpath, ignoreGeneralValue = ignoreGeneralValue)
|
|
|
|
def getConfigDoc(self):
|
|
if self._configDoc is None:
|
|
fileSystem = self.fileSystem
|
|
fileSystemPath = self.getFileSystemPath()
|
|
if fileSystem.exists(fileSystemPath) and fileSystem.isdir(fileSystemPath):
|
|
fileSystemConfigPath = os.path.join(fileSystemPath, "_config.xml")
|
|
else:
|
|
fileSystemConfigPath = fileSystemPath + ".config.xml"
|
|
absoluteConfigPath = fileSystem.getAbsolutePath(fileSystemConfigPath)
|
|
try:
|
|
configFile = fileSystem.open(fileSystemConfigPath, "rb")
|
|
except IOError, error:
|
|
if error.errno in (errno.ENOENT, errno.ENOTDIR):
|
|
# No such file or directory
|
|
configDoc = "none"
|
|
elif error.errno == errno.EACCES:
|
|
# Permission denied
|
|
logs.info("Can't access configuration file %s: permission denied."
|
|
% fileSystemConfigPath)
|
|
configDoc = "none"
|
|
else:
|
|
raise
|
|
else:
|
|
try:
|
|
try:
|
|
# FIXME: What to use for URL instead of None (second argument)?
|
|
if hasattr(configFile, "fileno"):
|
|
configDoc = libxml2.readFd(
|
|
configFile.fileno(), absoluteConfigPath, None,
|
|
libxml2.XML_PARSE_DTDLOAD | libxml2.XML_PARSE_NONET)
|
|
else:
|
|
# For zip files, ...
|
|
configDoc = libxml2.readDoc(
|
|
configFile.read(), absoluteConfigPath, None,
|
|
libxml2.XML_PARSE_DTDLOAD | libxml2.XML_PARSE_NONET)
|
|
except (libxml2.parserError, libxml2.treeError):
|
|
logs.info('[getConfigDog] Error parsing XML file at "%s".' %
|
|
fileSystemConfigPath)
|
|
raise
|
|
finally:
|
|
configFile.close()
|
|
self._configDoc = configDoc
|
|
if self._configDoc == "none":
|
|
return None
|
|
return self._configDoc
|
|
|
|
def getConfigElementNodesAndOwner(self, namespaceUri, name, xpath, ignoreGeneralValue = False):
|
|
if namespaceUri is None:
|
|
return [], self
|
|
configDoc = self.getConfigDoc()
|
|
elementNodes = xpaths.evaluateXpath(
|
|
"yep:element[@namespace = '%s' and @name = '%s']" % (
|
|
namespaceUri.replace("'", "'"), name.replace("'", "'")),
|
|
contextNode = configDoc)
|
|
if elementNodes:
|
|
elementNode = elementNodes[0]
|
|
nodes = xpaths.evaluateXpath(xpath, contextNode = elementNode)
|
|
if nodes:
|
|
return nodes, self
|
|
if not ignoreGeneralValue:
|
|
nodes = xpaths.evaluateXpath(xpath, contextNode = configDoc)
|
|
if nodes:
|
|
return nodes, self
|
|
return super(DataHolder, self).getConfigElementNodesAndOwner(
|
|
namespaceUri, name, xpath, ignoreGeneralValue = ignoreGeneralValue)
|
|
|
|
def getConfigNodesAndOwner(self, xpath, ignoreGeneralValue = False):
|
|
if not self.mimeType:
|
|
self.mimeType = 'application/octet-stream'
|
|
logs.debug('Invalid MIME type for %s' % self)
|
|
return self.getConfigDataHolderNodesAndOwner(
|
|
self.mimeType, xpath, ignoreGeneralValue = ignoreGeneralValue)
|
|
|
|
def getConfigStationNodesAndOwner(self, xpath):
|
|
nodes = xpaths.evaluateXpath(xpath, contextNode = self.getConfigDoc())
|
|
if nodes:
|
|
return nodes, self
|
|
return super(DataHolder, self).getConfigStationNodesAndOwner(xpath)
|
|
|
|
def getContainedFileSystem(self):
|
|
if self._containedFileSystem is None:
|
|
return self.fileSystem
|
|
else:
|
|
return self._containedFileSystem
|
|
|
|
def getContainedFileSystemPath(self):
|
|
if self._containedFileSystem is not None:
|
|
return ""
|
|
return super(DataHolder, self).getContainedFileSystemPath()
|
|
|
|
def getData(self):
|
|
if self._data is None:
|
|
self._data = self.loadData()
|
|
if self._data == "none":
|
|
return None
|
|
return self._data
|
|
|
|
def getDataFile(self):
|
|
return self.fileSystem.open(self.getFileSystemPath(), "rb")
|
|
|
|
def getDirectoryAbsolutePath(self):
|
|
parent = self.getParent()
|
|
if parent is None:
|
|
absolutePath = "/"
|
|
else:
|
|
absolutePath = parent.getAbsolutePath()
|
|
if self.pathFragment:
|
|
absolutePath = os.path.dirname(os.path.normpath(os.path.join(
|
|
absolutePath, self.pathFragment)))
|
|
return absolutePath
|
|
|
|
def getDocument(self):
|
|
return self
|
|
|
|
def getFileSystem(self):
|
|
parent = self.getParent()
|
|
if parent is None:
|
|
return self._containedFileSystem
|
|
parentHolder = parent.getDataHolder()
|
|
if parentHolder is None:
|
|
return self._containedFileSystem
|
|
return parentHolder.containedFileSystem
|
|
|
|
def getFileSystemPath(self):
|
|
parent = self.getParent()
|
|
if parent is None:
|
|
fileSystemPath = ""
|
|
else:
|
|
fileSystemPath = parent.getContainedFileSystemPath()
|
|
if self.pathFragment:
|
|
fileSystemPath = os.path.normpath(os.path.join(
|
|
fileSystemPath, self.pathFragment))
|
|
return fileSystemPath
|
|
|
|
def getMetadata(self):
|
|
if self._metadata is None:
|
|
metadata = None
|
|
metadataDoc = None
|
|
fileSystem = self.fileSystem
|
|
fileSystemPath = self.getFileSystemPath()
|
|
if fileSystem.exists(fileSystemPath) and fileSystem.isdir(fileSystemPath):
|
|
fileSystemMetadataPath = os.path.join(fileSystemPath, "_meta.xml")
|
|
metadataHolderPathFragment = "_meta.xml"
|
|
metadataHolderParent = None # Use previous (= self).
|
|
else:
|
|
fileSystemMetadataPath = fileSystemPath + ".meta.xml"
|
|
metadataHolderPathFragment = self.pathFragment + ".meta.xml"
|
|
metadataHolderParent = self.getParent()
|
|
absoluteMetadataPath = fileSystem.getAbsolutePath(fileSystemMetadataPath)
|
|
try:
|
|
metadataFile = fileSystem.open(fileSystemMetadataPath, "rb")
|
|
except IOError, error:
|
|
if error.errno in (errno.ENOENT, errno.ENOTDIR):
|
|
# No such file or directory
|
|
pass
|
|
elif error.errno == errno.EACCES:
|
|
# Permission denied
|
|
logs.info("Can't access metadata file %s: permission denied."
|
|
% fileSystemMetadataPath)
|
|
metadata = "none"
|
|
else:
|
|
raise
|
|
else:
|
|
try:
|
|
try:
|
|
# FIXME: What to use for URL instead of None (second argument)?
|
|
if hasattr(metadataFile, "fileno"):
|
|
metadataDoc = libxml2.readFd(
|
|
metadataFile.fileno(), absoluteMetadataPath, None,
|
|
libxml2.XML_PARSE_DTDLOAD | libxml2.XML_PARSE_NONET)
|
|
else:
|
|
# For zip files, ...
|
|
metadataDoc = libxml2.readDoc(
|
|
metadataFile.read(), absoluteMetadataPath, None,
|
|
libxml2.XML_PARSE_DTDLOAD | libxml2.XML_PARSE_NONET)
|
|
except (libxml2.parserError, libxml2.treeError):
|
|
logs.info('[getMetadata] Error parsing XML file at "%s".' %
|
|
fileSystemMetadataPath)
|
|
finally:
|
|
metadataFile.close()
|
|
if metadata is None:
|
|
if metadataDoc is None:
|
|
metadataDoc = libxml2.newDoc("1.0")
|
|
metadataNode = metadataDoc.newTextChild(None, "metadata", None)
|
|
metadataNamespace = metadataNode.newNs(namespaces.yep.uri, None)
|
|
metadataNode.setNs(metadataNamespace)
|
|
metadataHolder = XmlHolder(
|
|
metadataDoc, pathFragment = metadataHolderPathFragment, previous = self,
|
|
parent = metadataHolderParent, uriPathFragment = "metadata")
|
|
metadata = metadataHolder.getRootElement()
|
|
self._metadata = metadata
|
|
if self._metadata == "none":
|
|
return None
|
|
return self._metadata
|
|
|
|
def getModificationTime(self):
|
|
fileSystemPath = self.getFileSystemPath()
|
|
if fileSystemPath is None:
|
|
return None
|
|
fileSystem = self.fileSystem
|
|
if fileSystem.exists(fileSystemPath):
|
|
return fileSystem.getModificationTime(fileSystemPath)
|
|
else:
|
|
return None
|
|
|
|
def getNewActionsByLabel(self):
|
|
actionsByLabel = {}
|
|
|
|
for namespaceName, name in modules.getHolderConstructorNamespaceAndElementNames():
|
|
action = "new-%s-%s" % (namespaceName, name)
|
|
label = _("New %s") % name
|
|
actionsByLabel[label] = action
|
|
|
|
import elements
|
|
nodesAndOwnerCouples = self.getAllConfigElementNodesAndOwnerCouples(None)
|
|
for nodes, owner in nodesAndOwnerCouples.values():
|
|
node = nodes[0]
|
|
pythonClassNodes = xpaths.evaluateXpath("yep:pythonClass", contextNode = node)
|
|
if not pythonClassNodes:
|
|
continue
|
|
pythonClass = self.convertNodesToPythonClass(
|
|
pythonClassNodes, owner, "yep:pythonClass", default = None)
|
|
if pythonClass is None:
|
|
continue
|
|
if not issubclass(pythonClass, elements.RootElement):
|
|
continue
|
|
nameNodes = xpaths.evaluateXpath("@name", contextNode = node)
|
|
if not nameNodes:
|
|
continue
|
|
name = nameNodes[0].content
|
|
namespaceUriNodes = xpaths.evaluateXpath("@namespace", contextNode = node)
|
|
if not namespaceUriNodes:
|
|
continue
|
|
namespaceUri = namespaceUriNodes[0].content
|
|
namespaceName = namespaces.getName(namespaceUri)
|
|
action = "new-%s-%s" % (namespaceName, name)
|
|
label = _("New %s") % name
|
|
actionsByLabel[label] = action
|
|
|
|
for label, action in actionsByLabel.items():
|
|
if not self.walk([action], "GET", "existsAndIsAuthorized"):
|
|
del actionsByLabel[label]
|
|
return actionsByLabel
|
|
|
|
def getSimpleLabel(self):
|
|
return _("Untitled Object #%s") % self.localId
|
|
|
|
def getSiteXslt(self):
|
|
if self._xslt is None:
|
|
xslAbsolutePath = self.getConfigAbsolutePath("yep:siteXsltFilePath", default = None)
|
|
if xslAbsolutePath is None:
|
|
self._xslt == "none"
|
|
else:
|
|
xsltHolder = DataHolder(
|
|
pathFragment = xslAbsolutePath, previous = self, mimeType = "text/xml",
|
|
isRootElder = True,
|
|
containedFileSystem = filesystems.PartialFileSystem(xslAbsolutePath))
|
|
import stylesheets
|
|
xsltPrototype = xsltHolder.getRootElement()
|
|
self._xslt = stylesheets.StylesheetContext(xsltPrototype, self, previous = self)
|
|
if self._xslt == "none":
|
|
return None
|
|
return self._xslt
|
|
|
|
def getWebDavContentLength(self):
|
|
if self._data is None:
|
|
try:
|
|
dataFile = self.getDataFile()
|
|
except IOError, error:
|
|
if error.errno in (errno.ENOENT, errno.ENOTDIR):
|
|
# No such file or directory
|
|
raise faults.PathNotFound("")
|
|
if error.errno == errno.EACCES:
|
|
# Permission denied
|
|
raise faults.PathForbidden("")
|
|
raise
|
|
if hasattr(dataFile, "fileno"):
|
|
dataSize = os.fstat(dataFile.fileno())[6]
|
|
else:
|
|
# For StringIO and cStringIO classes.
|
|
dataSize = len(dataFile.getvalue())
|
|
else:
|
|
dataSize = len(self._data)
|
|
return dataSize
|
|
|
|
def getWebDavContentType(self):
|
|
return self.mimeType
|
|
|
|
def getWebDavLastModified(self):
|
|
modificationTime = self.getModificationTime()
|
|
if modificationTime is None:
|
|
return None
|
|
else:
|
|
return time.strftime("%a, %d %b %Y %H:%M:%S GMT", modificationTime)
|
|
|
|
def loadData(self):
|
|
dataFile = self.getDataFile()
|
|
data = dataFile.read()
|
|
dataFile.close()
|
|
return data
|
|
|
|
def metadata(self, *uriPathFragments):
|
|
metadata = self.getMetadata()
|
|
if metadata is None:
|
|
raise faults.PathNotFound("metadata")
|
|
return metadata.checkAccessAndWalk(
|
|
uriPathFragments, environs.getVar("httpCommand"), environs.getVar("instruction"))
|
|
|
|
def register(cls):
|
|
if cls.defaultFileNameExtension and cls.mimeType \
|
|
and cls.defaultFileNameExtension not in mimeTypes:
|
|
logs.info("Registering extension: %s" % cls.defaultFileNameExtension)
|
|
mimeTypes[cls.defaultFileNameExtension] = cls.mimeType
|
|
if cls.mimeTypes:
|
|
classMimeTypes = cls.mimeTypes[:]
|
|
else:
|
|
classMimeTypes = []
|
|
if cls.mimeType and cls.mimeType not in classMimeTypes:
|
|
classMimeTypes.append(cls.mimeType)
|
|
for mimeType in classMimeTypes:
|
|
if mimeType in holderClassesByMimeType:
|
|
if not issubclass(cls, holderClassesByMimeType[mimeType]):
|
|
raise Exception("""\
|
|
Error while registering class %s: MIME type "%s" is already used for class %s.\
|
|
""" % (
|
|
cls, mimeType, holderClassesByMimeType[mimeType]))
|
|
else:
|
|
holderClassesByMimeType[mimeType] = cls
|
|
register = classmethod(register)
|
|
|
|
def saveDataFile(self, sourceDataFile, length = None):
|
|
"""Save data into file and return True if the file has been newly created."""
|
|
path = self.getFileSystemPath()
|
|
isNewlyCreated = not self.fileSystem.access(path, os.F_OK)
|
|
if isNewlyCreated:
|
|
directoryPath = os.path.dirname(path)
|
|
if not self.fileSystem.access(directoryPath, os.F_OK):
|
|
self.fileSystem.makedirs(directoryPath)
|
|
dataFile = self.fileSystem.open(path, "wb")
|
|
if length is None:
|
|
while True:
|
|
chunk = sourceDataFile.read(1048576) # 1 MB chunk
|
|
if not chunk:
|
|
break
|
|
dataFile.write(chunk)
|
|
else:
|
|
count = 0
|
|
while count < length:
|
|
chunkSize = min(1048576, length - count)
|
|
chunk = sourceDataFile.read(chunkSize)
|
|
if not chunk:
|
|
break
|
|
dataFile.write(chunk)
|
|
count += len(chunk)
|
|
dataFile.close()
|
|
if self._data is not None:
|
|
del self._data
|
|
return isNewlyCreated
|
|
|
|
def setContainedFileSystem(self, containedFileSystem):
|
|
self._containedFileSystem = containedFileSystem
|
|
|
|
containedFileSystem = property(getContainedFileSystem, setContainedFileSystem)
|
|
data = property(getData)
|
|
fileSystem = property(getFileSystem)
|
|
simpleLabel = property(getSimpleLabel)
|
|
|
|
|
|
class StaticDataHolder(DataHolder):
|
|
styled = None # By defaut a static object has no HTML output.
|
|
unstyled = None # By defaut a static object has no HTML output.
|
|
|
|
def doHttpGet(self):
|
|
return self.source()
|
|
|
|
def getSimplestDownloadUrl(self):
|
|
# By defaut, a data holder can be download.
|
|
# A download is like asking for the source, but the download is forced
|
|
# at the browser step.
|
|
return self.getActionUri("download")
|
|
|
|
def getSimplestSourceUrl(self):
|
|
# By defaut, this is the source of a data holder that is sent.
|
|
return self.getUri()
|
|
|
|
def getSimplestStyledUrl(self):
|
|
# By default, a data holder has no X(HT)ML representation.
|
|
return None
|
|
|
|
def outputHttpDownload(self):
|
|
# Try to use a file instead of a string (so that it doesn't fail with
|
|
# large files).
|
|
try:
|
|
data = self.getDataFile()
|
|
except IOError, error:
|
|
if error.errno == errno.ENOENT:
|
|
# No such file or directory
|
|
raise faults.PathNotFound("")
|
|
if error.errno == errno.EACCES:
|
|
# Permission denied
|
|
raise faults.PathForbidden("")
|
|
raise
|
|
if data is None:
|
|
data = self.data
|
|
|
|
headers = None
|
|
if self.uriPathFragment:
|
|
headers = {
|
|
"Content-Disposition": "attachment; filename=\"%s\"" % self.uriPathFragment
|
|
}
|
|
|
|
environs.getVar("httpRequestHandler").outputData(
|
|
data, mimeType = self.mimeType, modificationTime = self.getModificationTime(),
|
|
headers = headers)
|
|
|
|
|
|
def outputHttpSource(self):
|
|
# Try to use a file instead of a string (so that it doesn't fail with
|
|
# large files).
|
|
try:
|
|
data = self.getDataFile()
|
|
except IOError, error:
|
|
if error.errno == errno.ENOENT:
|
|
# No such file or directory
|
|
raise faults.PathNotFound("")
|
|
if error.errno == errno.EACCES:
|
|
# Permission denied
|
|
raise faults.PathForbidden("")
|
|
raise
|
|
if data is None:
|
|
data = self.data
|
|
|
|
charset = self.getConfigString("yep:charset", default = None)
|
|
|
|
environs.getVar("httpRequestHandler").outputData(
|
|
data, mimeType = self.mimeType, modificationTime = self.getModificationTime(),
|
|
charset = charset)
|
|
|
|
|
|
class ImageHolder(StaticDataHolder):
|
|
mimeTypes = ("image/gif", "image/jpeg", "image/png", "image/svg+xml")
|
|
|
|
def generateXml(self, layout):
|
|
import html
|
|
layout.append(html.html(html.body(html.img(src = self.getUri()))))
|
|
return True
|
|
|
|
def generateXmlDocument(self):
|
|
htmlDocument = documents.newTemporaryHtmlDocument()
|
|
self.generateXml(htmlDocument)
|
|
return htmlDocument
|
|
|
|
def getSimplestStyledUrl(self):
|
|
return self.getActionUri("styled")
|
|
|
|
def styled(self):
|
|
# Skip StaticDataHolder (un)definition of styled.
|
|
super(StaticDataHolder, self).styled()
|
|
|
|
def unstyled(self):
|
|
# Skip StaticDataHolder (un)definition of styled.
|
|
super(StaticDataHolder, self).unstyled()
|
|
|
|
|
|
class PythonHolder(StaticDataHolder):
|
|
defaultFileNameExtension = ".py"
|
|
mimeType = "text/x-python-src"
|
|
|
|
def walk(self, uriPathFragments, command = None, instruction = None):
|
|
if not self.getInRawMode():
|
|
modeNames = self.getConfigString("yep:defaultMode", default = "view").lower()
|
|
for modeName in modeNames.split():
|
|
if modeName == "execute":
|
|
return self.fileSystem.execute(self.getFileSystemPath(), globals(), locals())
|
|
elif modeName == "view":
|
|
break
|
|
else:
|
|
logs.info('Ignoring unknown mode "%s" for PythonHolder at "%s".' % (
|
|
modeName, self.getAbsolutePath()))
|
|
return super(PythonHolder, self).walk(uriPathFragments, command, instruction)
|
|
|
|
|
|
class TextPlainHolder(StaticDataHolder):
|
|
mimeType = "text/plain"
|
|
|
|
def generatePlainText(self):
|
|
return self.data.decode("UTF-8")
|
|
|
|
def generateXml(self, layout):
|
|
import html
|
|
layout.append(html.html(html.body(html.pre(self.data))))
|
|
return True
|
|
|
|
def generateXmlDocument(self):
|
|
htmlDocument = documents.newTemporaryHtmlDocument()
|
|
self.generateXml(htmlDocument)
|
|
return htmlDocument
|
|
|
|
def getSimplestStyledUrl(self):
|
|
return self.getActionUri("styled")
|
|
|
|
def styled(self):
|
|
# Skip StaticDataHolder (un)definition of styled.
|
|
super(StaticDataHolder, self).styled()
|
|
|
|
def unstyled(self):
|
|
# Skip StaticDataHolder (un)definition of styled.
|
|
super(StaticDataHolder, self).unstyled()
|
|
|
|
|
|
class XmlHolder(DataHolder, documents.AbstractDocument):
|
|
_xmlHolderInited = False
|
|
defaultFileNameExtension = ".xml"
|
|
isTemporary = False
|
|
mimeType = "text/xml"
|
|
|
|
def __del__(self):
|
|
DataHolder.__del__(self)
|
|
documents.AbstractDocument.__del__(self)
|
|
|
|
def __init__(self, node = None, *arguments, **keywords):
|
|
if self._xmlHolderInited:
|
|
return
|
|
self._xmlHolderInited = True
|
|
|
|
DataHolder.__init__(self, *arguments, **keywords)
|
|
documents.AbstractDocument.__init__(self, node)
|
|
self.changeClass()
|
|
|
|
def changeClass(self):
|
|
try:
|
|
node = self.getRootNode()
|
|
except libxml2.treeError:
|
|
# we got here with "[EX] Parsing failed"; go back to StaticDataHolder and
|
|
# set mimetype to text/plain
|
|
# Note: maybe we should instead return an error page?
|
|
self.__class__ = StaticDataHolder
|
|
self.mimeType = "text/plain"
|
|
return
|
|
if node is not None:
|
|
try:
|
|
namespaceUri = node.ns().content
|
|
except libxml2.treeError:
|
|
# The libxml2 Python binding raises this exception when xmlNs returns None.
|
|
namespaceUri = None
|
|
realClass = modules.getHolderClass(namespaceUri, node.name, default = self.__class__)
|
|
if realClass != self.__class__ and issubclass(realClass, self.__class__):
|
|
self.__class__ = realClass
|
|
|
|
def deletePublicName(self):
|
|
self.getRootNode().unsetProp("publicName")
|
|
|
|
def destroy(self):
|
|
parent = self.getParent()
|
|
if parent is not None:
|
|
parent.itemDestroyed(self)
|
|
self.fileSystem.remove(self.getFileSystemPath())
|
|
|
|
def doHttpGet(self):
|
|
if not self.getInRawMode():
|
|
rootElement = self.getRootElement()
|
|
if rootElement is not None:
|
|
try:
|
|
return rootElement.checkAccessAndWalk(
|
|
[], environs.getVar("httpCommand"), environs.getVar("instruction"))
|
|
except (faults.PathForbidden, faults.PathNotFound, faults.PathUnauthorized), fault:
|
|
logs.exception("Ignoring fault %s in %s.doHttpGet()" % (fault, rootElement))
|
|
return super(XmlHolder, self).doHttpGet()
|
|
|
|
def doHttpPost(self):
|
|
if not self.getInRawMode():
|
|
rootElement = self.getRootElement()
|
|
if rootElement is not None:
|
|
try:
|
|
return rootElement.checkAccessAndWalk(
|
|
[], environs.getVar("httpCommand"), environs.getVar("instruction"))
|
|
except (faults.PathForbidden, faults.PathNotFound, faults.PathUnauthorized), fault:
|
|
logs.exception("Ignoring fault %s in %s.doHttpPost()" % (fault, rootElement))
|
|
return super(XmlHolder, self).doHttpPost()
|
|
|
|
def generatePlainText(self):
|
|
rootElement = self.getRootElement()
|
|
if rootElement is not None:
|
|
return rootElement.generateXml(layout)
|
|
return super(TemporaryDocument, self).generatePlainText()
|
|
|
|
def generateXml(self, layout):
|
|
rootElement = self.getRootElement()
|
|
if rootElement is not None:
|
|
return rootElement.generateXml(layout)
|
|
return super(XmlHolder, self).generateXml(layout)
|
|
|
|
def generateXmlDocument(self):
|
|
rootElement = self.getRootElement()
|
|
if rootElement is not None:
|
|
return rootElement.generateXmlDocument()
|
|
return super(XmlHolder, self).generateXmlDocument()
|
|
|
|
def getBind(self, context, modelContext):
|
|
return None
|
|
|
|
def getChildInstanceDataXpath(self, context, modelContext):
|
|
return ""
|
|
|
|
def getDataHolder(self):
|
|
return self
|
|
|
|
def getLocalId(self):
|
|
return self.pathFragment
|
|
|
|
def getModelId(self):
|
|
return None
|
|
|
|
def getNode(self):
|
|
if self._node is None:
|
|
if self.isRemote:
|
|
raise Exception("FIXME, TODO: download data.")
|
|
else:
|
|
doc = self.parseDataFile()
|
|
self._node = doc
|
|
if self.isRootElder:
|
|
self.publicName = self.getAbsolutePath()
|
|
else:
|
|
self.publicName = self.localId
|
|
doc.xincludeProcess()
|
|
|
|
# Now that we know what is the root node, we can change the class of the holder.
|
|
# Note: The change of class can be done quite late after the instantiation of holder.
|
|
self.changeClass()
|
|
return self._node
|
|
|
|
def getPublicName(self):
|
|
return self.getRootNode().prop("publicName")
|
|
|
|
def isAccessAuthorized(self, command):
|
|
rootElement = self.getRootElement()
|
|
if rootElement is not None:
|
|
return rootElement.isAccessAuthorized(command)
|
|
return super(XmlHolder, self).isAccessAuthorized(command)
|
|
|
|
def isContentAccessAuthorized(self, contentName, command):
|
|
rootElement = self.getRootElement()
|
|
if rootElement is not None:
|
|
return rootElement.isContentAccessAuthorized(contentName, command)
|
|
return super(XmlHolder, self).isContentAccessAuthorized(contentName, command)
|
|
|
|
def outputHttpSource(self):
|
|
environs.getVar("httpRequestHandler").outputData(
|
|
self.getData(), mimeType = self.mimeType,
|
|
modificationTime = self.getModificationTime())
|
|
|
|
def parseDataFile(self):
|
|
dataFile = self.getDataFile()
|
|
try:
|
|
try:
|
|
# The code below had several problems :
|
|
# - The parseXxx functions are deprecated (use readXxx
|
|
# instead).
|
|
# - htmlParseDoc doesn't set the namespaces.
|
|
# - parseDoc doesn't work with XHTML files containing
|
|
# entities like é, etc.
|
|
# if self.mimeType == 'text/html':
|
|
# # FIXME: None instead of "UTF-8" doesn't work for
|
|
# # unicode files.
|
|
# self.node = libxml2.htmlParseDoc(data, "UTF-8")
|
|
# else:
|
|
# self.node = libxml2.parseDoc(data)
|
|
|
|
# FIXME: For performance reasons, we should use readFd
|
|
# instead of readDoc, whithout reading data first.
|
|
# FIXME: I'm not sure what to use as URL argument.
|
|
# Option XML_PARSE_DTDLOAD is needed for XHTML documents
|
|
# containing entities liek é, etc. Option
|
|
# XML_PARSE_DTDATTR also works, but I don't know which
|
|
# option is best.
|
|
# Option XML_PARSE_NONET ensures no network access will be
|
|
# done (but local XML catalog is still used). Setting this
|
|
# option ensures that no "invisible" net access is done
|
|
# and a warning is logged when one is required.
|
|
if hasattr(dataFile, "fileno"):
|
|
doc = libxml2.readFd(
|
|
dataFile.fileno(), self.getAbsolutePath(), None,
|
|
libxml2.XML_PARSE_DTDLOAD | libxml2.XML_PARSE_NONET)
|
|
else:
|
|
# For zip files, ...
|
|
doc = libxml2.readDoc(
|
|
dataFile.read(), self.getAbsolutePath(), None,
|
|
libxml2.XML_PARSE_DTDLOAD | libxml2.XML_PARSE_NONET)
|
|
except (libxml2.parserError, libxml2.treeError), e:
|
|
logs.info('[parseDataFile] Error parsing XML file at "%s".' % self.getAbsolutePath())
|
|
# this is an error parsing but libxml2 may raise treeError;
|
|
# <grin>; let's modify the message
|
|
e.msg = "[EX] Parsing failed"
|
|
raise
|
|
finally:
|
|
dataFile.close()
|
|
return doc
|
|
|
|
def save(self):
|
|
publicName = self.publicName
|
|
localId = self.localId
|
|
if publicName and self.defaultFileNameExtension and "." not in publicName:
|
|
publicName += self.defaultFileNameExtension
|
|
if publicName and publicName != localId:
|
|
isRenamed = True
|
|
oldLocalId = localId
|
|
oldFileSystemPath = self.getFileSystemPath()
|
|
localId = publicName
|
|
self.pathFragment = localId
|
|
if self.uriPathFragment == oldLocalId:
|
|
self.uriPathFragment = localId
|
|
else:
|
|
isRenamed = False
|
|
del self.publicName
|
|
data = self.serialize()
|
|
self.publicName = publicName
|
|
fileSystem = self.fileSystem
|
|
fileSystemPath = self.getFileSystemPath()
|
|
fileSystemDirectoryPath = os.path.dirname(fileSystemPath)
|
|
if not self.fileSystem.access(fileSystemDirectoryPath, os.F_OK):
|
|
self.fileSystem.makedirs(fileSystemDirectoryPath)
|
|
if isRenamed:
|
|
try:
|
|
fileSystem.rename(oldFileSystemPath, fileSystemPath)
|
|
except OSError:
|
|
logs.debug('oldFileSystemPath = "%s", fileSystemPath = "%s"' % (
|
|
oldFileSystemPath, fileSystemPath))
|
|
raise
|
|
dataFile = fileSystem.open(fileSystemPath, "wb")
|
|
dataFile.write(data)
|
|
dataFile.close()
|
|
if isRenamed:
|
|
parent = self.getParent()
|
|
if parent is not None:
|
|
parent.itemRenamed(self, oldLocalId)
|
|
|
|
def setNode(self, node):
|
|
super(XmlHolder, self).setNode(node)
|
|
self.changeClass()
|
|
|
|
def setPermanentLocalId(self):
|
|
publicName = self.publicName
|
|
if publicName:
|
|
localId = publicName
|
|
else:
|
|
localId = self.getParent().newItemLocalId()
|
|
if self.defaultFileNameExtension and "." not in localId:
|
|
localId += self.defaultFileNameExtension
|
|
if self.uriPathFragment == self.pathFragment:
|
|
self.uriPathFragment = localId
|
|
self.pathFragment = localId
|
|
if self.isTemporary:
|
|
del self.isTemporary
|
|
|
|
def setPublicName(self, publicName):
|
|
self.getRootNode().setProp("publicName", publicName)
|
|
|
|
def walk(self, uriPathFragments, command = None, instruction = None):
|
|
try:
|
|
return super(XmlHolder, self).walk(uriPathFragments, command, instruction)
|
|
except (faults.PathForbidden, faults.PathNotFound, faults.PathUnauthorized), fault:
|
|
rootElement = self.getRootElement()
|
|
if rootElement is None:
|
|
raise
|
|
return rootElement.checkAccessAndWalk(uriPathFragments, command, instruction)
|
|
|
|
localId = property(getLocalId)
|
|
modelId = property(getModelId)
|
|
node = property(getNode, setNode, documents.AbstractDocument.deleteNode)
|
|
publicName = property(getPublicName, setPublicName, deletePublicName)
|
|
|
|
|
|
def newXmlHolder(namespaceUri, elementName, directoryHolder, pathFragment = None,
|
|
uriPathFragment = None, fileNameExtension = ".xml", temporary = False):
|
|
assert isinstance(directoryHolder, DataHolder)
|
|
if pathFragment is None:
|
|
if temporary:
|
|
namespaceName = namespaces.getName(namespaceUri)
|
|
itemLocalId = "new-%s-%s" % (namespaceName, elementName)
|
|
else:
|
|
itemLocalId = directoryHolder.newItemLocalId()
|
|
if fileNameExtension:
|
|
itemLocalId = itemLocalId + fileNameExtension
|
|
if uriPathFragment is None:
|
|
uriPathFragment = itemLocalId
|
|
pathFragment = itemLocalId
|
|
itemDoc = libxml2.newDoc("1.0")
|
|
itemRootNode = itemDoc.newTextChild(None, elementName, None)
|
|
if namespaceUri == namespaces.yep.uri:
|
|
namespacePrefix = None
|
|
else:
|
|
namespacePrefix = namespaces.getName(namespaceUri)
|
|
itemNamespace = itemRootNode.newNs(namespaceUri, namespacePrefix)
|
|
itemRootNode.setNs(itemNamespace)
|
|
itemHolder = XmlHolder(itemDoc, pathFragment = pathFragment, previous = directoryHolder,
|
|
uriPathFragment = uriPathFragment)
|
|
itemHolder.isTemporary = temporary
|
|
return itemHolder
|
|
|
|
|
|
ImageHolder.register()
|
|
PythonHolder.register()
|
|
TextPlainHolder.register()
|
|
XmlHolder.register()
|