bugfix.
[Plinn.git] / Products / Plinn / AttachmentTool.py
1 # -*- coding: utf-8 -*-
2 #######################################################################################
3 # Plinn - http://plinn.org #
4 # © 2007-2014 Benoît Pin <pin@cri.ensmp.fr> #
5 # #
6 # This program is free software; you can redistribute it and/or #
7 # modify it under the terms of the GNU General Public License #
8 # as published by the Free Software Foundation; either version 2 #
9 # of the License, or (at your option) any later version. #
10 # #
11 # This program is distributed in the hope that it will be useful, #
12 # but WITHOUT ANY WARRANTY; without even the implied warranty of #
13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the #
14 # GNU General Public License for more details. #
15 # #
16 # You should have received a copy of the GNU General Public License #
17 # along with this program; if not, write to the Free Software #
18 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. #
19 #######################################################################################
20 """ Portal attachment management tool.
21
22
23
24 """
25
26 from AccessControl import ClassSecurityInfo
27 from Acquisition import aq_base
28 from Globals import InitializeClass
29 from OFS.SimpleItem import SimpleItem
30 from OFS.Folder import Folder
31 from OFS.Image import cookId
32 from Products.Photo.blobbases import File
33 from zExceptions import Unauthorized
34 from zExceptions import BadRequest
35 from Products.Photo import Photo
36 from Products.CMFCore.utils import UniqueObject, getToolByName, getUtilityByInterfaceName
37 from Products.CMFCore.permissions import ModifyPortalContent
38 from Products.CMFCore.exceptions import AccessControl_Unauthorized
39 from Products.Plinn.utils import makeValidId
40
41 from urllib import unquote
42 from cgi import escape
43 from ZServer import LARGE_FILE_THRESHOLD
44 from webdav.interfaces import IWriteLock
45 from webdav.common import Locked
46 from webdav.common import PreconditionFailed
47 from zope.contenttype import guess_content_type
48
49 from libxml2 import HTML_PARSE_RECOVER, HTML_PARSE_NOERROR, HTML_PARSE_NOWARNING
50 from libxml2 import htmlReadDoc
51 PARSE_OPTIONS = HTML_PARSE_RECOVER + HTML_PARSE_NOERROR + HTML_PARSE_NOWARNING
52
53 class AttachmentTool( UniqueObject, SimpleItem):
54 """ Links attachment objects to contents.
55 """
56
57 id = 'portal_attachment'
58 meta_type = 'Attachment Tool'
59 manage_options = SimpleItem.manage_options
60
61 security = ClassSecurityInfo()
62
63 security.declarePublic('getAttachmentsFor')
64 def getAttachmentsFor(self, content):
65 """getAttachmentsFor returns attachments container of content
66 """
67 if getattr( aq_base(content), 'attachments', None ) is None :
68 self._createAttachmentContainerFor(content)
69
70 return content.attachments
71
72 security.declarePrivate('_createAttachmentContainerFor')
73 def _createAttachmentContainerFor(self, content):
74 """_createAttachmentContainerFor documentation
75 """
76
77 content.attachments = AttachmentContainer()
78
79 security.declarePublic('uploadAttachmentFor')
80 def uploadAttachmentFor(self, content, file, title='', typeName='File') :
81 "upload attachment inside content's attachment folder."
82
83 mtool = getToolByName(self, 'portal_membership')
84 if not mtool.checkPermission(ModifyPortalContent, content) :
85 raise AccessControl_Unauthorized
86
87 utool = getToolByName(self, 'portal_url')
88 portal = utool.getPortalObject()
89
90 attachments = self.getAttachmentsFor(content)
91 dummy, title = cookId('', title, file)
92 id = makeValidId(attachments, title)
93
94 if typeName == 'Photo':
95 thumbSize = {'thumb_height' : portal.getProperty('thumb_size', 192),
96 'thumb_width' : portal.getProperty('thumb_size', 192)}
97 fileOb = Photo(id, title, file, **thumbSize)
98 elif typeName == 'File' :
99 fileOb = File(id, title, '')
100 fileOb.manage_upload(file)
101 else :
102 raise AccessControl_Unauthorized
103
104 content.attachments._setObject(id, fileOb)
105 fileOb = getattr(content.attachments, id)
106 return fileOb
107
108
109
110 InitializeClass( AttachmentTool )
111
112
113 class AttachmentContainer (Folder):
114
115 meta_type = 'Attachment container'
116 security = ClassSecurityInfo()
117
118 def __init__(self):
119 self.id = 'attachments'
120
121 security.declarePrivate('checkIdAvailable')
122 def checkIdAvailable(self, id):
123 try:
124 self._checkId(id)
125 except BadRequest:
126 return False
127 else:
128 return True
129
130
131 security.declareProtected(ModifyPortalContent, 'put_upload')
132 def put_upload(self, REQUEST, RESPONSE):
133 """ Upload a content thru webdav put method.
134 The default behavior (NullRessource.PUT + PortalFolder.PUT_factory)
135 disallow files names with '_' at the begining.
136 """
137
138 self.dav__init(REQUEST, RESPONSE)
139 fileName = unquote(REQUEST.getHeader('X-File-Name', ''))
140 validId = makeValidId(self, fileName, allow_dup=True)
141
142 ifhdr = REQUEST.get_header('If', '')
143 if self.wl_isLocked():
144 if ifhdr:
145 self.dav__simpleifhandler(REQUEST, RESPONSE, col=1)
146 else:
147 raise Locked
148 elif ifhdr:
149 raise PreconditionFailed
150
151 if int(REQUEST.get('CONTENT_LENGTH') or 0) > LARGE_FILE_THRESHOLD:
152 file = REQUEST['BODYFILE']
153 body = file.read(LARGE_FILE_THRESHOLD)
154 file.seek(0)
155 else:
156 body = REQUEST.get('BODY', '')
157
158 typ=REQUEST.get_header('content-type', None)
159 if typ is None:
160 typ, enc=guess_content_type(validId, body)
161
162 if self.checkIdAvailable(validId) :
163 if typ.startswith('image/') :
164 utool = getUtilityByInterfaceName('Products.CMFCore.interfaces.IURLTool')
165 portal = utool.getPortalObject()
166 thumbSize = {'thumb_height' : portal.getProperty('thumb_size', 192),
167 'thumb_width' : portal.getProperty('thumb_size', 192)}
168 ob = Photo(validId, fileName, '', **thumbSize)
169 else :
170 ob = File(validId, fileName, '')
171
172 self._setObject(validId, ob)
173 httpRespCode = 201
174 else :
175 httpRespCode = 200
176 ob = self._getOb(validId)
177
178 # We call _verifyObjectPaste with verify_src=0, to see if the
179 # user can create this type of object (and we don't need to
180 # check the clipboard.
181 try:
182 self._verifyObjectPaste(ob.__of__(self), 0)
183 except CopyError:
184 sMsg = 'Unable to create object of class %s in %s: %s' % \
185 (ob.__class__, repr(self), sys.exc_info()[1],)
186 raise Unauthorized, sMsg
187
188 ob.PUT(REQUEST, RESPONSE)
189 RESPONSE.setStatus(httpRespCode)
190 RESPONSE.setHeader('Content-Type', 'text/xml;;charset=utf-8')
191 if ob.meta_type == 'Blob File' :
192 return '<element id="%s" title="%s"/>' % (ob.getId(), escape(ob.title_or_id()))
193 elif ob.meta_type == 'Photo' :
194 width, height = ob.getResizedImageSize(size=(310, 310))
195 return '<element src="%(src)s" title="%(title)s" width="%(width)d" height="%(height)d"/>' % \
196 {'src' : 'attachments/%s/getResizedImage?size=%d_%d' % (ob.getId(), width, height),
197 'title' : escape(ob.title_or_id()),
198 'width' : width,
199 'height' : height
200 }
201
202 security.declareProtected(ModifyPortalContent, 'removeUnusedAttachments')
203 def removeUnusedAttachments(self, html) :
204 html = '<div>%s</div>' % html
205 doc = htmlReadDoc(html, '', None, PARSE_OPTIONS)
206 used = {}
207
208 hrefs = doc.xpathEval('//a/@href')
209 for href in [a.get_content() for a in hrefs] :
210 if href.startswith('attachments/') :
211 used[href[len('attachments/'):]] = True
212
213 srcs = doc.xpathEval('//img/@src')
214 for src in [a.get_content() for a in srcs] :
215 if src.startswith('attachments/') :
216 parts = src.split('/')
217 if len(parts) >=2 :
218 used[parts[1]] = True
219
220 unused = [id for id in self.objectIds() if not used.has_key(id)]
221 if unused :
222 self.manage_delObjects(unused)
223
224 InitializeClass(AttachmentContainer)