Attachment 'AttachFile_base170_modified.py'
Download 1 # -*- coding: iso-8859-1 -*-
2 """
3 MoinMoin - AttachFile action
4
5 This action lets a page have multiple attachment files.
6 It creates a folder <data>/pages/<pagename>/attachments
7 and keeps everything in there.
8
9 Form values: action=Attachment
10 1. with no 'do' key: returns file upload form
11 2. do=attach: accept file upload and saves the file in
12 ../attachment/pagename/
13 3. /pagename/fname?action=Attachment&do=get[&mimetype=type]:
14 return contents of the attachment file with the name fname.
15 4. /pathname/fname, do=view[&mimetype=type]:create a page
16 to view the content of the file
17
18 To link to an attachment, use [[attachment:file.txt]],
19 to embed an attachment, use {{attachment:file.png}}.
20
21 This module uses optional configure;
22 attach_fname_method:
23 to support unicode filename for attached file.
24 "170compatible", "u16le", "punycode", "punycodesplit",
25 and "urllib" available. if not set, "punycode" is
26 used. see pydoc for details.
27 attach_fname_prefix:
28 used for prefix of unicode filename on server's
29 filesystem. if not set, default prefix of each method
30 used("le_", "xn--", or "ps_"). "170compatible" and
31 "urllib" don't use this parameter.
32
33 @copyright: 2001 by Ken Sugino (sugino@mediaone.net),
34 2001-2004 by Juergen Hermann <jh@web.de>,
35 2005 MoinMoin:AlexanderSchremmer,
36 2005 DiegoOngaro at ETSZONE (diego@etszone.com),
37 2005-2007 MoinMoin:ReimarBauer,
38 2007-2008 MoinMoin:ThomasWaldmann
39 (FILENAME_CODING work by 2008 Suzumizaki-Kimitaka)
40 @license: GNU GPL, see COPYING for details.
41 """
42
43 import os, time, zipfile, mimetypes, errno, urllib
44
45 from MoinMoin import log
46 logging = log.getLogger(__name__)
47
48 from MoinMoin import config, wikiutil, packages
49 from MoinMoin.Page import Page
50 from MoinMoin.util import filesys, timefuncs
51 from MoinMoin.security.textcha import TextCha
52 from MoinMoin.events import FileAttachedEvent, send_event
53
54 action_name = __name__.split('.')[-1]
55
56 #############################################################################
57 ### Encoding filename support
58 #############################################################################
59
60 class CodingBase:
61 def __init__(self, prefix = "", default = ""):
62 if prefix == "empty":
63 prefix = ""
64 elif prefix == "":
65 prefix = default
66 self.my_prefix = prefix
67 return
68
69 def isPrefixMatch(self, s):
70 return s.startswith(self.my_prefix)
71
72 def eatPrefix(self, s):
73 return s[len(self.my_prefix):]
74
75 def assumeNonUnicodeObject(self, s):
76 """make sure the string is not unicode object.
77
78 This function gives because punycode_decode raises
79 UnicodeError on the line:
80 base = unicode(base, 'ascii', errors)
81 Of course, given s must be consists of ascii chars.
82 """
83 if isinstance(s, str):
84 return s
85 r = ""
86 for i in s:
87 r += chr(ord(i))
88 return r
89
90 def isAscii(self, s):
91 for i in s:
92 if ord(i)>0x7f:
93 return False
94 return True
95
96 class Invalid170Coding(CodingBase):
97 """Encode/Decode with Illegal method. Only used for compatibility.
98
99 This method is used during about 1.5.8 - 1.7.0, but simply
100 WRONG implementation. because built-in functions open(), file(),
101 or the functions in "os" modules should NOT be used with
102 unicode filename which is byte-encoded. Only 'unicode string type'
103 is allowed as unicode filename(or until be raised Exception, you
104 can use encode(sys.getfilesystemencoding()).
105 Again, this method is wrong and deprecated. Don't use with new wiki.
106 """
107 def __init__(self, prefix=""):
108 CodingBase.__init__(self, prefix, "")
109
110 def encode(self, s):
111 if isinstance(s, unicode):
112 return s.encode(config.charset)
113 return s
114
115 def decode(self, s):
116 return s.decode(config.charset)
117
118 class Utf16leCoding(CodingBase):
119 """Encode/Decode with Simple 16bit little endian hex.
120
121 This method is simple one and easy to understand.
122 You can use instead of punycode that requires python 2.3,
123 and when wiki uses Non-Ascii names a lot with attached file,
124 this method makes more shorter file name than urllib.quote().
125 Default prefix is "le_".
126 """
127 def __init__(self, prefix=""):
128 CodingBase.__init__(self, prefix, "le_")
129 return
130
131 def encode(self, s):
132 if self.isAscii(s):
133 if not self.isPrefixMatch(s):
134 return s
135 return self.my_prefix+unicode(s).encode("UTF-16LE").encode("hex")
136
137 def decode(self, s):
138 if self.isPrefixMatch(s):
139 return self.eatPrefix(s).decode("hex").decode("UTF-16LE")
140 return s
141
142 class PunycodeCoding(CodingBase):
143 """Encode/Decode with punycode. Requires Python 2.3 or later.
144
145 This method makes filename shorter than others. The method
146 correcting ascii characters first, so extentions on file system
147 looks like many file types(but just "looks like").
148 Default prefix is "xn--".
149 """
150 def __init__(self, prefix=""):
151 CodingBase.__init__(self, prefix, "xn--")
152 return
153
154 def encode(self, s):
155 if self.isAscii(s):
156 if not self.isPrefixMatch(s):
157 return s
158 return self.my_prefix+s.encode("punycode")
159
160 def decode(self, s):
161 s = self.assumeNonUnicodeObject(s)
162 if self.isPrefixMatch(s):
163 return self.eatPrefix(s).decode("punycode")
164 return s
165
166 class PunycodeSplitCoding(CodingBase):
167 """Encode/Decode with punycode but splitting filename extention.
168
169 Requires Python 2.3 or later.
170 This method is similer with using simple punycode, but extension
171 is splitted before encode. This makes easier to find filetype on
172 file system used by wiki, but encoding/decoding are complex some.
173 And of course, this method losts compatibility with punycoded
174 uris, just be ascii uris.
175 Default prefix is "ps_".
176 """
177 def __init__(self, prefix=""):
178 CodingBase.__init__(self, prefix, "ps_")
179 return
180
181 def encode(self, s):
182 r = ""
183 for i, c in enumerate(os.path.splitext(s)):
184 if self.isPrefixMatch(c[i:]) or not self.isAscii(c):
185 r += c[:i]+self.my_prefix+ \
186 (unicode(c[i:]).encode("punycode"))
187 else:
188 r += c
189 return r
190
191 def decode(self, s):
192 s = self.assumeNonUnicodeObject(s)
193 r = ""
194 for i, c in enumerate(os.path.splitext(s)):
195 if self.isPrefixMatch(c[i:]):
196 r += c[:i]+(self.eatPrefix(c[i:]).decode("punycode"))
197 else:
198 r += c
199 return r
200
201 class UrllibCoding(CodingBase):
202 """Encode/Decode with urllib.quote/unquote.
203
204 This method no needs to use prefix like others.
205 This method may be compatible with Trac.
206 This method makes filename very longer than others.
207 """
208 def __init__(self, prefix=""):
209 CodingBase.__init__(self, prefix, "")
210
211 def encode(self, s):
212 return urllib.quote(unicode(s).encode("utf-8"))
213
214 def decode(self, s):
215 return urllib.unquote(s).decode("utf-8")
216
217 coding_obj = ""
218 def _co(request):
219 global coding_obj
220 if coding_obj:
221 return coding_obj
222 coding_names = { "170compatible":Invalid170Coding,
223 "u16le":Utf16leCoding,
224 "punycode":PunycodeCoding,
225 "punycodesplit":PunycodeSplitCoding,
226 "urllib":UrllibCoding }
227 coding_pf = ""
228 coding_method = "punycode"
229 if hasattr(request.cfg, "attach_fname_prefix"):
230 coding_pf = request.cfg.attach_fname_prefix
231 if hasattr(request.cfg, "attach_fname_method"):
232 if request.cfg.attach_fname_method in coding_names:
233 coding_method = request.cfg.attach_fname_method
234 else:
235 s = reduce(lambda x,y: x+", "+y, \
236 coding_names.keys()[1:], coding_names.keys()[0])
237 logging.error(\
238 'attach_fname_method="%(n)s" is invalid.' \
239 ' Should be one of %(g)s.' \
240 % { "n": request.cfg.attach_fname_method, "g":s })
241 coding_obj = coding_names[coding_method](coding_pf)
242 logging.info("Name coding method used by attached file is %s" % coding_obj)
243 return coding_obj
244
245 #############################################################################
246 ### External interface - these are called from the core code
247 #############################################################################
248
249 class AttachmentAlreadyExists(Exception):
250 pass
251
252
253 def getBasePath(request):
254 """ Get base path where page dirs for attachments are stored. """
255 return request.rootpage.getPagePath('pages')
256
257
258 def getAttachDir(request, pagename, create=0):
259 """ Get directory where attachments for page `pagename` are stored. """
260 if request.page and pagename == request.page.page_name:
261 page = request.page # reusing existing page obj is faster
262 else:
263 page = Page(request, pagename)
264 return page.getPagePath("attachments", check_create=create)
265
266
267 def absoluteName(url, pagename):
268 """ Get (pagename, filename) of an attachment: link
269 @param url: PageName/filename.ext or filename.ext (unicode)
270 @param pagename: name of the currently processed page (unicode)
271 @rtype: tuple of unicode
272 @return: PageName, filename.ext
273 """
274 url = wikiutil.AbsPageName(pagename, url)
275 pieces = url.split(u'/')
276 if len(pieces) == 1:
277 return pagename, pieces[0]
278 else:
279 return u"/".join(pieces[:-1]), pieces[-1]
280
281
282 def attachUrl(request, pagename, filename=None, **kw):
283 # filename is not used yet, but should be used later to make a sub-item url
284 if kw:
285 qs = '?%s' % wikiutil.makeQueryString(kw, want_unicode=False)
286 else:
287 qs = ''
288 return "%s/%s%s" % (request.getScriptname(), wikiutil.quoteWikinameURL(pagename), qs)
289
290
291 def getAttachUrl(pagename, filename, request, addts=0, escaped=0, do='get', drawing='', upload=False):
292 """ Get URL that points to attachment `filename` of page `pagename`. """
293 if upload:
294 if not drawing:
295 url = attachUrl(request, pagename, filename,
296 rename=wikiutil.taintfilename(filename), action=action_name)
297 else:
298 url = attachUrl(request, pagename, filename,
299 rename=wikiutil.taintfilename(filename), drawing=drawing, action=action_name)
300 else:
301 if not drawing:
302 url = attachUrl(request, pagename, filename,
303 target=filename, action=action_name, do=do)
304 else:
305 url = attachUrl(request, pagename, filename,
306 drawing=drawing, action=action_name)
307 if escaped:
308 url = wikiutil.escape(url)
309 return url
310
311
312 def getIndicator(request, pagename):
313 """ Get an attachment indicator for a page (linked clip image) or
314 an empty string if not attachments exist.
315 """
316 _ = request.getText
317 attach_dir = getAttachDir(request, pagename)
318 if not os.path.exists(attach_dir):
319 return ''
320
321 files = os.listdir(attach_dir)
322 if not files:
323 return ''
324
325 fmt = request.formatter
326 attach_count = _('[%d attachments]') % len(files)
327 attach_icon = request.theme.make_icon('attach', vars={'attach_count': attach_count})
328 attach_link = (fmt.url(1, attachUrl(request, pagename, action=action_name), rel='nofollow') +
329 attach_icon +
330 fmt.url(0))
331 return attach_link
332
333
334 def getFilename(request, pagename, filename):
335 """ make complete pathfilename of file "name" attached to some page "pagename"
336 @param request: request object
337 @param pagename: name of page where the file is attached to (unicode)
338 @param filename: filename of attached file (unicode)
339 @rtype: string (in filename_coding encoding)
340 @return: complete path/filename of attached file
341 """
342 filename = _co(request).encode(filename)
343 return os.path.join(getAttachDir(request, pagename, create=1), filename)
344
345
346 def exists(request, pagename, filename):
347 """ check if page <pagename> has a file <filename> attached """
348 fpath = getFilename(request, pagename, filename)
349 return os.path.exists(fpath)
350
351
352 def size(request, pagename, filename):
353 """ return file size of file attachment """
354 fpath = getFilename(request, pagename, filename)
355 return os.path.getsize(fpath)
356
357
358 def info(pagename, request):
359 """ Generate snippet with info on the attachment for page `pagename`. """
360 _ = request.getText
361
362 attach_dir = getAttachDir(request, pagename)
363 files = []
364 if os.path.isdir(attach_dir):
365 files = os.listdir(attach_dir)
366 page = Page(request, pagename)
367 link = page.url(request, {'action': action_name})
368 attach_info = _('There are <a href="%(link)s">%(count)s attachment(s)</a> stored for this page.') % {
369 'count': len(files),
370 'link': wikiutil.escape(link)
371 }
372 return "\n<p>\n%s\n</p>\n" % attach_info
373
374
375 def _write_stream(content, stream, bufsize=8192):
376 if hasattr(content, 'read'): # looks file-like
377 import shutil
378 shutil.copyfileobj(content, stream, bufsize)
379 elif isinstance(content, str):
380 stream.write(content)
381 else:
382 logging.error("unsupported content object: %r" % content)
383 raise
384
385 def add_attachment(request, pagename, target, filecontent, overwrite=0):
386 """ save <filecontent> to an attachment <target> of page <pagename>
387
388 filecontent can be either a str (in memory file content),
389 or an open file object (file content in e.g. a tempfile).
390 """
391 _ = request.getText
392
393 # replace illegal chars
394 target = wikiutil.taintfilename(target)
395
396 # get directory, and possibly create it
397 attach_dir = getAttachDir(request, pagename, create=1)
398 # save file
399 fpath = os.path.join(attach_dir, _co(request).encode(target))
400 exists = os.path.exists(fpath)
401 if exists and not overwrite:
402 raise AttachmentAlreadyExists
403 else:
404 if exists:
405 try:
406 os.remove(fpath)
407 except:
408 pass
409 stream = open(fpath, 'wb')
410 try:
411 _write_stream(filecontent, stream)
412 finally:
413 stream.close()
414
415 _addLogEntry(request, 'ATTNEW', pagename, target)
416
417 filesize = os.path.getsize(fpath)
418 event = FileAttachedEvent(request, pagename, target, filesize)
419 send_event(event)
420
421 return target, filesize
422
423
424 #############################################################################
425 ### Internal helpers
426 #############################################################################
427
428 def _addLogEntry(request, action, pagename, filename):
429 """ Add an entry to the edit log on uploads and deletes.
430
431 `action` should be "ATTNEW" or "ATTDEL"
432 """
433 from MoinMoin.logfile import editlog
434 t = wikiutil.timestamp2version(time.time())
435 fname = wikiutil.url_quote(filename, want_unicode=True)
436
437 # Write to global log
438 log = editlog.EditLog(request)
439 log.add(request, t, 99999999, action, pagename, request.remote_addr, fname)
440
441 # Write to local log
442 log = editlog.EditLog(request, rootpagename=pagename)
443 log.add(request, t, 99999999, action, pagename, request.remote_addr, fname)
444
445
446 def _access_file(pagename, request):
447 """ Check form parameter `target` and return a tuple of
448 `(pagename, filename, filepath)` for an existing attachment.
449
450 Return `(pagename, None, None)` if an error occurs.
451 """
452 _ = request.getText
453
454 error = None
455 if not request.form.get('target', [''])[0]:
456 error = _("Filename of attachment not specified!")
457 else:
458 filename = wikiutil.taintfilename(request.form['target'][0])
459 fpath = getFilename(request, pagename, filename)
460
461 if os.path.isfile(fpath):
462 return (pagename, filename, fpath)
463 error = _("Attachment '%(filename)s' does not exist!") % {'filename': filename}
464
465 error_msg(pagename, request, error)
466 return (pagename, None, None)
467
468
469 def _build_filelist(request, pagename, showheader, readonly, mime_type='*'):
470 _ = request.getText
471 fmt = request.html_formatter
472
473 # access directory
474 attach_dir = getAttachDir(request, pagename)
475 files = _get_files(request, pagename)
476
477 if mime_type != '*':
478 files = [fname for fname in files if mime_type == mimetypes.guess_type(fname)[0]]
479
480 html = []
481 if files:
482 if showheader:
483 html.append(fmt.rawHTML(_(
484 "To refer to attachments on a page, use '''{{{attachment:filename}}}''', \n"
485 "as shown below in the list of files. \n"
486 "Do '''NOT''' use the URL of the {{{[get]}}} link, \n"
487 "since this is subject to change and can break easily.",
488 wiki=True
489 )))
490
491 label_del = _("del")
492 label_move = _("move")
493 label_get = _("get")
494 label_edit = _("edit")
495 label_view = _("view")
496 label_unzip = _("unzip")
497 label_install = _("install")
498
499 html.append(fmt.bullet_list(1))
500 for file in files:
501 mt = wikiutil.MimeType(filename=file)
502 fullpath = os.path.join(attach_dir, _co(request).encode(file))
503 st = os.stat(fullpath)
504 base, ext = os.path.splitext(file)
505 parmdict = {'file': wikiutil.escape(file),
506 'fsize': "%.1f" % (float(st.st_size) / 1024),
507 'fmtime': request.user.getFormattedDateTime(st.st_mtime),
508 }
509
510 links = []
511 may_delete = request.user.may.delete(pagename)
512 if may_delete and not readonly:
513 links.append(fmt.url(1, getAttachUrl(pagename, file, request, do='del')) +
514 fmt.text(label_del) +
515 fmt.url(0))
516
517 if may_delete and not readonly:
518 links.append(fmt.url(1, getAttachUrl(pagename, file, request, do='move')) +
519 fmt.text(label_move) +
520 fmt.url(0))
521
522 links.append(fmt.url(1, getAttachUrl(pagename, file, request)) +
523 fmt.text(label_get) +
524 fmt.url(0))
525
526 if ext == '.draw':
527 links.append(fmt.url(1, getAttachUrl(pagename, file, request, drawing=base)) +
528 fmt.text(label_edit) +
529 fmt.url(0))
530 else:
531 links.append(fmt.url(1, getAttachUrl(pagename, file, request, do='view')) +
532 fmt.text(label_view) +
533 fmt.url(0))
534
535 try:
536 is_zipfile = zipfile.is_zipfile(fullpath)
537 if is_zipfile:
538 is_package = packages.ZipPackage(request, fullpath).isPackage()
539 if is_package and request.user.isSuperUser():
540 links.append(fmt.url(1, getAttachUrl(pagename, file, request, do='install')) +
541 fmt.text(label_install) +
542 fmt.url(0))
543 elif (not is_package and mt.minor == 'zip' and
544 may_delete and
545 request.user.may.read(pagename) and
546 request.user.may.write(pagename)):
547 links.append(fmt.url(1, getAttachUrl(pagename, file, request, do='unzip')) +
548 fmt.text(label_unzip) +
549 fmt.url(0))
550 except RuntimeError:
551 # We don't want to crash with a traceback here (an exception
552 # here could be caused by an uploaded defective zip file - and
553 # if we crash here, the user does not get a UI to remove the
554 # defective zip file again).
555 # RuntimeError is raised by zipfile stdlib module in case of
556 # problems (like inconsistent slash and backslash usage in the
557 # archive).
558 logging.exception("An exception within zip file attachment handling occurred:")
559
560 html.append(fmt.listitem(1))
561 html.append("[%s]" % " | ".join(links))
562 html.append(" (%(fmtime)s, %(fsize)s KB) [[attachment:%(file)s]]" % parmdict)
563 html.append(fmt.listitem(0))
564 html.append(fmt.bullet_list(0))
565
566 else:
567 if showheader:
568 html.append(fmt.paragraph(1))
569 html.append(fmt.text(_("No attachments stored for %(pagename)s") % {
570 'pagename': pagename}))
571 html.append(fmt.paragraph(0))
572
573 return ''.join(html)
574
575
576 def _get_files(request, pagename):
577 attach_dir = getAttachDir(request, pagename)
578 if os.path.isdir(attach_dir):
579 files = [_co(request).decode(fn) for fn in os.listdir(attach_dir)]
580 files.sort()
581 else:
582 files = []
583 return files
584
585
586 def _get_filelist(request, pagename):
587 return _build_filelist(request, pagename, 1, 0)
588
589
590 def error_msg(pagename, request, msg):
591 request.theme.add_msg(msg, "error")
592 Page(request, pagename).send_page()
593
594
595 #############################################################################
596 ### Create parts of the Web interface
597 #############################################################################
598
599 def send_link_rel(request, pagename):
600 files = _get_files(request, pagename)
601 for fname in files:
602 url = getAttachUrl(pagename, fname, request, do='view', escaped=1)
603 request.write(u'<link rel="Appendix" title="%s" href="%s">\n' % (
604 wikiutil.escape(fname), url))
605
606
607 def send_hotdraw(pagename, request):
608 _ = request.getText
609
610 now = time.time()
611 pubpath = request.cfg.url_prefix_static + "/applets/TWikiDrawPlugin"
612 basename = request.form['drawing'][0]
613 drawpath = getAttachUrl(pagename, basename + '.draw', request, escaped=1)
614 pngpath = getAttachUrl(pagename, basename + '.png', request, escaped=1)
615 pagelink = attachUrl(request, pagename, '', action=action_name, ts=now)
616 helplink = Page(request, "HelpOnActions/AttachFile").url(request)
617 savelink = attachUrl(request, pagename, '', action=action_name, do='savedrawing')
618 #savelink = Page(request, pagename).url(request) # XXX include target filename param here for twisted
619 # request, {'savename': request.form['drawing'][0]+'.draw'}
620 #savelink = '/cgi-bin/dumpform.bat'
621
622 timestamp = '&ts=%s' % now
623
624 request.write('<h2>' + _("Edit drawing") + '</h2>')
625 request.write("""
626 <p>
627 <img src="%(pngpath)s%(timestamp)s">
628 <applet code="CH.ifa.draw.twiki.TWikiDraw.class"
629 archive="%(pubpath)s/twikidraw.jar" width="640" height="480">
630 <param name="drawpath" value="%(drawpath)s">
631 <param name="pngpath" value="%(pngpath)s">
632 <param name="savepath" value="%(savelink)s">
633 <param name="basename" value="%(basename)s">
634 <param name="viewpath" value="%(pagelink)s">
635 <param name="helppath" value="%(helplink)s">
636 <strong>NOTE:</strong> You need a Java enabled browser to edit the drawing example.
637 </applet>
638 </p>""" % {
639 'pngpath': pngpath, 'timestamp': timestamp,
640 'pubpath': pubpath, 'drawpath': drawpath,
641 'savelink': savelink, 'pagelink': pagelink, 'helplink': helplink,
642 'basename': basename
643 })
644
645
646 def send_uploadform(pagename, request):
647 """ Send the HTML code for the list of already stored attachments and
648 the file upload form.
649 """
650 _ = request.getText
651
652 if not request.user.may.read(pagename):
653 request.write('<p>%s</p>' % _('You are not allowed to view this page.'))
654 return
655
656 writeable = request.user.may.write(pagename)
657
658 # First send out the upload new attachment form on top of everything else.
659 # This avoids usability issues if you have to scroll down a lot to upload
660 # a new file when the page already has lots of attachments:
661 if writeable:
662 request.write('<h2>' + _("New Attachment") + '</h2>')
663 request.write("""
664 <form action="%(baseurl)s/%(pagename)s" method="POST" enctype="multipart/form-data">
665 <dl>
666 <dt>%(upload_label_file)s</dt>
667 <dd><input type="file" name="file" size="50"></dd>
668 <dt>%(upload_label_rename)s</dt>
669 <dd><input type="text" name="rename" size="50" value="%(rename)s"></dd>
670 <dt>%(upload_label_overwrite)s</dt>
671 <dd><input type="checkbox" name="overwrite" value="1" %(overwrite_checked)s></dd>
672 </dl>
673 %(textcha)s
674 <p>
675 <input type="hidden" name="action" value="%(action_name)s">
676 <input type="hidden" name="do" value="upload">
677 <input type="submit" value="%(upload_button)s">
678 </p>
679 </form>
680 """ % {
681 'baseurl': request.getScriptname(),
682 'pagename': wikiutil.quoteWikinameURL(pagename),
683 'action_name': action_name,
684 'upload_label_file': _('File to upload'),
685 'upload_label_rename': _('Rename to'),
686 'rename': request.form.get('rename', [''])[0],
687 'upload_label_overwrite': _('Overwrite existing attachment of same name'),
688 'overwrite_checked': ('', 'checked')[request.form.get('overwrite', ['0'])[0] == '1'],
689 'upload_button': _('Upload'),
690 'textcha': TextCha(request).render(),
691 })
692
693 request.write('<h2>' + _("Attached Files") + '</h2>')
694 request.write(_get_filelist(request, pagename))
695
696 if not writeable:
697 request.write('<p>%s</p>' % _('You are not allowed to attach a file to this page.'))
698
699 if writeable and request.form.get('drawing', [None])[0]:
700 send_hotdraw(pagename, request)
701
702
703 #############################################################################
704 ### Web interface for file upload, viewing and deletion
705 #############################################################################
706
707 def execute(pagename, request):
708 """ Main dispatcher for the 'AttachFile' action. """
709 _ = request.getText
710
711 do = request.form.get('do', ['upload_form'])
712 handler = globals().get('_do_%s' % do[0])
713 if handler:
714 msg = handler(pagename, request)
715 else:
716 msg = _('Unsupported AttachFile sub-action: %s') % (wikiutil.escape(do[0]), )
717 if msg:
718 error_msg(pagename, request, msg)
719
720
721 def _do_upload_form(pagename, request):
722 upload_form(pagename, request)
723
724
725 def upload_form(pagename, request, msg=''):
726 _ = request.getText
727
728 request.emit_http_headers()
729 # Use user interface language for this generated page
730 request.setContentLanguage(request.lang)
731 request.theme.add_msg(msg, "dialog")
732 request.theme.send_title(_('Attachments for "%(pagename)s"') % {'pagename': pagename}, pagename=pagename)
733 request.write('<div id="content">\n') # start content div
734 send_uploadform(pagename, request)
735 request.write('</div>\n') # end content div
736 request.theme.send_footer(pagename)
737 request.theme.send_closing_html()
738
739
740 def preprocess_filename(filename):
741 """ preprocess the filename we got from upload form,
742 strip leading drive and path (IE misbehaviour)
743 """
744 if filename and len(filename) > 1 and (filename[1] == ':' or filename[0] == '\\'): # C:.... or \path... or \\server\...
745 bsindex = filename.rfind('\\')
746 if bsindex >= 0:
747 filename = filename[bsindex+1:]
748 return filename
749
750
751 def _do_upload(pagename, request):
752 _ = request.getText
753 # Currently we only check TextCha for upload (this is what spammers ususally do),
754 # but it could be extended to more/all attachment write access
755 if not TextCha(request).check_answer_from_form():
756 return _('TextCha: Wrong answer! Go back and try again...')
757
758 form = request.form
759 overwrite = form.get('overwrite', [u'0'])[0]
760 try:
761 overwrite = int(overwrite)
762 except:
763 overwrite = 0
764
765 if not request.user.may.write(pagename):
766 return _('You are not allowed to attach a file to this page.')
767
768 if overwrite and not request.user.may.delete(pagename):
769 return _('You are not allowed to overwrite a file attachment of this page.')
770
771 filename = form.get('file__filename__')
772 rename = form.get('rename', [u''])[0].strip()
773 if rename:
774 target = rename
775 else:
776 target = filename
777
778 target = preprocess_filename(target)
779 target = wikiutil.clean_input(target)
780
781 if not target:
782 return _("Filename of attachment not specified!")
783
784 # get file content
785 filecontent = request.form.get('file', [None])[0]
786 if filecontent is None:
787 # This might happen when trying to upload file names
788 # with non-ascii characters on Safari.
789 return _("No file content. Delete non ASCII characters from the file name and try again.")
790
791 # add the attachment
792 try:
793 target, bytes = add_attachment(request, pagename, target, filecontent, overwrite=overwrite)
794 msg = _("Attachment '%(target)s' (remote name '%(filename)s')"
795 " with %(bytes)d bytes saved.") % {
796 'target': target, 'filename': filename, 'bytes': bytes}
797 except AttachmentAlreadyExists:
798 msg = _("Attachment '%(target)s' (remote name '%(filename)s') already exists.") % {
799 'target': target, 'filename': filename}
800
801 # return attachment list
802 upload_form(pagename, request, msg)
803
804
805 def _do_savedrawing(pagename, request):
806 _ = request.getText
807
808 if not request.user.may.write(pagename):
809 return _('You are not allowed to save a drawing on this page.')
810
811 filename = request.form['filename'][0]
812 filecontent = request.form['filepath'][0]
813
814 basepath, basename = os.path.split(filename)
815 basename, ext = os.path.splitext(basename)
816
817 # get directory, and possibly create it
818 attach_dir = getAttachDir(request, pagename, create=1)
819 savepath = os.path.join(attach_dir, basename + ext)
820
821 if ext == '.draw':
822 _addLogEntry(request, 'ATTDRW', pagename, basename + ext)
823 filecontent = filecontent.read() # read file completely into memory
824 filecontent = filecontent.replace("\r", "")
825 elif ext == '.map':
826 filecontent = filecontent.read() # read file completely into memory
827 filecontent = filecontent.strip()
828
829 if filecontent:
830 # filecontent is either a file or a non-empty string
831 stream = open(savepath, 'wb')
832 try:
833 _write_stream(filecontent, stream)
834 finally:
835 stream.close()
836 else:
837 # filecontent is empty string (e.g. empty map file), delete the target file
838 try:
839 os.unlink(savepath)
840 except OSError, err:
841 if err.errno != errno.ENOENT: # no such file
842 raise
843
844 # touch attachment directory to invalidate cache if new map is saved
845 if ext == '.map':
846 os.utime(attach_dir, None)
847
848 request.emit_http_headers()
849 request.write("OK")
850
851
852 def _do_del(pagename, request):
853 _ = request.getText
854
855 pagename, filename, fpath = _access_file(pagename, request)
856 if not request.user.may.delete(pagename):
857 return _('You are not allowed to delete attachments on this page.')
858 if not filename:
859 return # error msg already sent in _access_file
860
861 # delete file
862 os.remove(fpath)
863 _addLogEntry(request, 'ATTDEL', pagename, filename)
864
865 if request.cfg.xapian_search:
866 from MoinMoin.search.Xapian import Index
867 index = Index(request)
868 if index.exists:
869 index.remove_item(pagename, filename)
870
871 upload_form(pagename, request, msg=_("Attachment '%(filename)s' deleted.") % {'filename': filename})
872
873
874 def move_file(request, pagename, new_pagename, attachment, new_attachment):
875 _ = request.getText
876
877 newpage = Page(request, new_pagename)
878 if newpage.exists(includeDeleted=1) and request.user.may.write(new_pagename) and request.user.may.delete(pagename):
879 new_attachment_path = os.path.join(getAttachDir(request, new_pagename,
880 create=1), _co(request).encode(new_attachment))
881 attachment_path = os.path.join(getAttachDir(request, pagename),
882 _co(request).encode(attachment))
883
884 if os.path.exists(new_attachment_path):
885 upload_form(pagename, request,
886 msg=_("Attachment '%(new_pagename)s/%(new_filename)s' already exists.") % {
887 'new_pagename': new_pagename,
888 'new_filename': new_attachment})
889 return
890
891 if new_attachment_path != attachment_path:
892 # move file
893 filesys.rename(attachment_path, new_attachment_path)
894 _addLogEntry(request, 'ATTDEL', pagename, attachment)
895 _addLogEntry(request, 'ATTNEW', new_pagename, new_attachment)
896 upload_form(pagename, request,
897 msg=_("Attachment '%(pagename)s/%(filename)s' moved to '%(new_pagename)s/%(new_filename)s'.") % {
898 'pagename': pagename,
899 'filename': attachment,
900 'new_pagename': new_pagename,
901 'new_filename': new_attachment})
902 else:
903 upload_form(pagename, request, msg=_("Nothing changed"))
904 else:
905 upload_form(pagename, request, msg=_("Page '%(new_pagename)s' does not exist or you don't have enough rights.") % {
906 'new_pagename': new_pagename})
907
908
909 def _do_attachment_move(pagename, request):
910 _ = request.getText
911
912 if 'cancel' in request.form:
913 return _('Move aborted!')
914 if not wikiutil.checkTicket(request, request.form['ticket'][0]):
915 return _('Please use the interactive user interface to move attachments!')
916 if not request.user.may.delete(pagename):
917 return _('You are not allowed to move attachments from this page.')
918
919 if 'newpagename' in request.form:
920 new_pagename = request.form.get('newpagename')[0]
921 else:
922 upload_form(pagename, request, msg=_("Move aborted because new page name is empty."))
923 if 'newattachmentname' in request.form:
924 new_attachment = request.form.get('newattachmentname')[0]
925 if new_attachment != wikiutil.taintfilename(new_attachment):
926 upload_form(pagename, request, msg=_("Please use a valid filename for attachment '%(filename)s'.") % {
927 'filename': new_attachment})
928 return
929 else:
930 upload_form(pagename, request, msg=_("Move aborted because new attachment name is empty."))
931
932 attachment = request.form.get('oldattachmentname')[0]
933 move_file(request, pagename, new_pagename, attachment, new_attachment)
934
935
936 def _do_move(pagename, request):
937 _ = request.getText
938
939 pagename, filename, fpath = _access_file(pagename, request)
940 if not request.user.may.delete(pagename):
941 return _('You are not allowed to move attachments from this page.')
942 if not filename:
943 return # error msg already sent in _access_file
944
945 # move file
946 d = {'action': action_name,
947 'baseurl': request.getScriptname(),
948 'do': 'attachment_move',
949 'ticket': wikiutil.createTicket(request),
950 'pagename': pagename,
951 'pagename_quoted': wikiutil.quoteWikinameURL(pagename),
952 'attachment_name': filename,
953 'move': _('Move'),
954 'cancel': _('Cancel'),
955 'newname_label': _("New page name"),
956 'attachment_label': _("New attachment name"),
957 }
958 formhtml = '''
959 <form action="%(baseurl)s/%(pagename_quoted)s" method="POST">
960 <input type="hidden" name="action" value="%(action)s">
961 <input type="hidden" name="do" value="%(do)s">
962 <input type="hidden" name="ticket" value="%(ticket)s">
963 <table>
964 <tr>
965 <td class="label"><label>%(newname_label)s</label></td>
966 <td class="content">
967 <input type="text" name="newpagename" value="%(pagename)s" size="80">
968 </td>
969 </tr>
970 <tr>
971 <td class="label"><label>%(attachment_label)s</label></td>
972 <td class="content">
973 <input type="text" name="newattachmentname" value="%(attachment_name)s" size="80">
974 </td>
975 </tr>
976 <tr>
977 <td></td>
978 <td class="buttons">
979 <input type="hidden" name="oldattachmentname" value="%(attachment_name)s">
980 <input type="submit" name="move" value="%(move)s">
981 <input type="submit" name="cancel" value="%(cancel)s">
982 </td>
983 </tr>
984 </table>
985 </form>''' % d
986 thispage = Page(request, pagename)
987 request.theme.add_msg(formhtml, "dialog")
988 return thispage.send_page()
989
990
991 def _do_get(pagename, request):
992 _ = request.getText
993
994 pagename, filename, fpath = _access_file(pagename, request)
995 if not request.user.may.read(pagename):
996 return _('You are not allowed to get attachments from this page.')
997 if not filename:
998 return # error msg already sent in _access_file
999
1000 timestamp = timefuncs.formathttpdate(int(os.path.getmtime(fpath)))
1001 if request.if_modified_since == timestamp:
1002 request.emit_http_headers(["Status: 304 Not modified"])
1003 else:
1004 mt = wikiutil.MimeType(filename=filename)
1005 content_type = mt.content_type()
1006 mime_type = mt.mime_type()
1007
1008 # TODO: fix the encoding here, plain 8 bit is not allowed according to the RFCs
1009 # There is no solution that is compatible to IE except stripping non-ascii chars
1010 # filename_enc = filename.encode(config. charset)
1011 filename_enc = _co(request).encode(filename)
1012
1013 # for dangerous files (like .html), when we are in danger of cross-site-scripting attacks,
1014 # we just let the user store them to disk ('attachment').
1015 # For safe files, we directly show them inline (this also works better for IE).
1016 dangerous = mime_type in request.cfg.mimetypes_xss_protect
1017 content_dispo = dangerous and 'attachment' or 'inline'
1018
1019 request.emit_http_headers([
1020 'Content-Type: %s' % content_type,
1021 'Last-Modified: %s' % timestamp,
1022 'Content-Length: %d' % os.path.getsize(fpath),
1023 'Content-Disposition: %s; filename="%s"' % (content_dispo, filename_enc),
1024 ])
1025
1026 # send data
1027 request.send_file(open(fpath, 'rb'))
1028
1029
1030 def _do_install(pagename, request):
1031 _ = request.getText
1032
1033 pagename, target, targetpath = _access_file(pagename, request)
1034 if not request.user.isSuperUser():
1035 return _('You are not allowed to install files.')
1036 if not target:
1037 return
1038
1039 package = packages.ZipPackage(request, targetpath)
1040
1041 if package.isPackage():
1042 if package.installPackage():
1043 msg = _("Attachment '%(filename)s' installed.") % {'filename': wikiutil.escape(target)}
1044 else:
1045 msg = _("Installation of '%(filename)s' failed.") % {'filename': wikiutil.escape(target)}
1046 if package.msg:
1047 msg += "<br><pre>%s</pre>" % wikiutil.escape(package.msg)
1048 else:
1049 msg = _('The file %s is not a MoinMoin package file.') % wikiutil.escape(target)
1050
1051 upload_form(pagename, request, msg=msg)
1052
1053
1054 def _do_unzip(pagename, request, overwrite=False):
1055 _ = request.getText
1056 pagename, filename, fpath = _access_file(pagename, request)
1057
1058 if not (request.user.may.delete(pagename) and request.user.may.read(pagename) and request.user.may.write(pagename)):
1059 return _('You are not allowed to unzip attachments of this page.')
1060
1061 if not filename:
1062 return # error msg already sent in _access_file
1063
1064 try:
1065 if not zipfile.is_zipfile(fpath):
1066 return _('The file %(filename)s is not a .zip file.') % {'filename': filename}
1067
1068 # determine how which attachment names we have and how much space each is occupying
1069 curr_fsizes = dict([(f, size(request, pagename, f)) for f in _get_files(request, pagename)])
1070
1071 # Checks for the existance of one common prefix path shared among
1072 # all files in the zip file. If this is the case, remove the common prefix.
1073 # We also prepare a dict of the new filenames->filesizes.
1074 zip_path_sep = '/' # we assume '/' is as zip standard suggests
1075 fname_index = None
1076 mapping = []
1077 new_fsizes = {}
1078 zf = zipfile.ZipFile(fpath)
1079 for zi in zf.infolist():
1080 name = zi.filename
1081 if not name.endswith(zip_path_sep): # a file (not a directory)
1082 if fname_index is None:
1083 fname_index = name.rfind(zip_path_sep) + 1
1084 path = name[:fname_index]
1085 if (name.rfind(zip_path_sep) + 1 != fname_index # different prefix len
1086 or
1087 name[:fname_index] != path): # same len, but still different
1088 mapping = [] # zip is not acceptable
1089 break
1090 if zi.file_size >= request.cfg.unzip_single_file_size: # file too big
1091 mapping = [] # zip is not acceptable
1092 break
1093 finalname = name[fname_index:] # remove common path prefix
1094 finalname = _co(request).decode(finalname)
1095 mapping.append((name, finalname))
1096 new_fsizes[finalname] = zi.file_size
1097
1098 # now we either have an empty mapping (if the zip is not acceptable),
1099 # an identity mapping (no subdirs in zip, just all flat), or
1100 # a mapping (origname, finalname) where origname is the zip member filename
1101 # (including some prefix path) and finalname is a simple filename.
1102
1103 # calculate resulting total file size / count after unzipping:
1104 if overwrite:
1105 curr_fsizes.update(new_fsizes)
1106 total = curr_fsizes
1107 else:
1108 new_fsizes.update(curr_fsizes)
1109 total = new_fsizes
1110 total_count = len(total)
1111 total_size = sum(total.values())
1112
1113 if not mapping:
1114 msg = _("Attachment '%(filename)s' not unzipped because some files in the zip "
1115 "are either not in the same directory or exceeded the single file size limit (%(maxsize_file)d kB)."
1116 ) % {'filename': filename,
1117 'maxsize_file': request.cfg.unzip_single_file_size / 1000, }
1118 elif total_size > request.cfg.unzip_attachments_space:
1119 msg = _("Attachment '%(filename)s' not unzipped because it would have exceeded "
1120 "the per page attachment storage size limit (%(size)d kB).") % {
1121 'filename': filename,
1122 'size': request.cfg.unzip_attachments_space / 1000, }
1123 elif total_count > request.cfg.unzip_attachments_count:
1124 msg = _("Attachment '%(filename)s' not unzipped because it would have exceeded "
1125 "the per page attachment count limit (%(count)d).") % {
1126 'filename': filename,
1127 'count': request.cfg.unzip_attachments_count, }
1128 else:
1129 not_overwritten = []
1130 for origname, finalname in mapping:
1131 try:
1132 # Note: reads complete zip member file into memory. ZipFile does not offer block-wise reading:
1133 add_attachment(request, pagename, finalname, zf.read(origname), overwrite)
1134 except AttachmentAlreadyExists:
1135 not_overwritten.append(finalname)
1136 if not_overwritten:
1137 msg = _("Attachment '%(filename)s' partially unzipped (did not overwrite: %(filelist)s).") % {
1138 'filename': filename,
1139 'filelist': ', '.join(not_overwritten), }
1140 else:
1141 msg = _("Attachment '%(filename)s' unzipped.") % {'filename': filename}
1142 except RuntimeError, err:
1143 # We don't want to crash with a traceback here (an exception
1144 # here could be caused by an uploaded defective zip file - and
1145 # if we crash here, the user does not get a UI to remove the
1146 # defective zip file again).
1147 # RuntimeError is raised by zipfile stdlib module in case of
1148 # problems (like inconsistent slash and backslash usage in the
1149 # archive).
1150 logging.exception("An exception within zip file attachment handling occurred:")
1151 msg = _("A severe error occurred:") + ' ' + str(err)
1152
1153 upload_form(pagename, request, msg=wikiutil.escape(msg))
1154
1155
1156 def send_viewfile(pagename, request):
1157 _ = request.getText
1158 fmt = request.html_formatter
1159
1160 pagename, filename, fpath = _access_file(pagename, request)
1161 if not filename:
1162 return
1163
1164 request.write('<h2>' + _("Attachment '%(filename)s'") % {'filename': filename} + '</h2>')
1165 # show a download link above the content
1166 label = _('Download')
1167 link = (fmt.url(1, getAttachUrl(pagename, filename, request, do='get'), css_class="download") +
1168 fmt.text(label) +
1169 fmt.url(0))
1170 request.write('%s<br><br>' % link)
1171
1172 mt = wikiutil.MimeType(filename=filename)
1173
1174 # destinguishs if browser need a plugin in place
1175 if mt.major == 'image' and mt.minor in config.browser_supported_images:
1176 request.write('<img src="%s" alt="%s">' % (
1177 getAttachUrl(pagename, filename, request, escaped=1),
1178 wikiutil.escape(filename, 1)))
1179 return
1180 elif mt.major == 'text':
1181 ext = os.path.splitext(filename)[1]
1182 Parser = wikiutil.getParserForExtension(request.cfg, ext)
1183 if Parser is not None:
1184 try:
1185 content = file(fpath, 'r').read()
1186 content = wikiutil.decodeUnknownInput(content)
1187 colorizer = Parser(content, request, filename=filename)
1188 colorizer.format(request.formatter)
1189 return
1190 except IOError:
1191 pass
1192
1193 request.write(request.formatter.preformatted(1))
1194 # If we have text but no colorizing parser we try to decode file contents.
1195 content = open(fpath, 'r').read()
1196 content = wikiutil.decodeUnknownInput(content)
1197 content = wikiutil.escape(content)
1198 request.write(request.formatter.text(content))
1199 request.write(request.formatter.preformatted(0))
1200 return
1201
1202 try:
1203 package = packages.ZipPackage(request, fpath)
1204 if package.isPackage():
1205 request.write("<pre><b>%s</b>\n%s</pre>" % (_("Package script:"), wikiutil.escape(package.getScript())))
1206 return
1207
1208 if zipfile.is_zipfile(fpath) and mt.minor == 'zip':
1209 zf = zipfile.ZipFile(fpath, mode='r')
1210 request.write("<pre>%-46s %19s %12s\n" % (_("File Name"), _("Modified")+" "*5, _("Size")))
1211 for zinfo in zf.filelist:
1212 date = "%d-%02d-%02d %02d:%02d:%02d" % zinfo.date_time
1213 request.write(wikiutil.escape("%-46s %s %12d\n" % (zinfo.filename, date, zinfo.file_size)))
1214 request.write("</pre>")
1215 return
1216 except RuntimeError:
1217 # We don't want to crash with a traceback here (an exception
1218 # here could be caused by an uploaded defective zip file - and
1219 # if we crash here, the user does not get a UI to remove the
1220 # defective zip file again).
1221 # RuntimeError is raised by zipfile stdlib module in case of
1222 # problems (like inconsistent slash and backslash usage in the
1223 # archive).
1224 logging.exception("An exception within zip file attachment handling occurred:")
1225 return
1226
1227 from MoinMoin import macro
1228 from MoinMoin.parser.text import Parser
1229
1230 macro.request = request
1231 macro.formatter = request.html_formatter
1232 p = Parser("##\n", request)
1233 m = macro.Macro(p)
1234
1235 # use EmbedObject to view valid mime types
1236 if mt is None:
1237 request.write('<p>' + _("Unknown file type, cannot display this attachment inline.") + '</p>')
1238 link = (fmt.url(1, getAttachUrl(pagename, filename, request)) +
1239 fmt.text(filename) +
1240 fmt.url(0))
1241 request.write('For using an external program follow this link %s' % link)
1242 return
1243 request.write(m.execute('EmbedObject', u'target=%s, pagename=%s' % (filename, pagename)))
1244 return
1245
1246
1247 def _do_view(pagename, request):
1248 _ = request.getText
1249
1250 orig_pagename = pagename
1251 pagename, filename, fpath = _access_file(pagename, request)
1252 if not request.user.may.read(pagename):
1253 return _('You are not allowed to view attachments of this page.')
1254 if not filename:
1255 return
1256
1257 # send header & title
1258 request.emit_http_headers()
1259 # Use user interface language for this generated page
1260 request.setContentLanguage(request.lang)
1261 title = _('attachment:%(filename)s of %(pagename)s') % {
1262 'filename': filename, 'pagename': pagename}
1263 request.theme.send_title(title, pagename=pagename)
1264
1265 # send body
1266 request.write(request.formatter.startContent())
1267 send_viewfile(orig_pagename, request)
1268 send_uploadform(pagename, request)
1269 request.write(request.formatter.endContent())
1270
1271 request.theme.send_footer(pagename)
1272 request.theme.send_closing_html()
1273
1274
1275 #############################################################################
1276 ### File attachment administration
1277 #############################################################################
1278
1279 def do_admin_browser(request):
1280 """ Browser for SystemAdmin macro. """
1281 from MoinMoin.util.dataset import TupleDataset, Column
1282 _ = request.getText
1283
1284 data = TupleDataset()
1285 data.columns = [
1286 Column('page', label=('Page')),
1287 Column('file', label=('Filename')),
1288 Column('size', label=_('Size'), align='right'),
1289 ]
1290
1291 # iterate over pages that might have attachments
1292 pages = request.rootpage.getPageList()
1293 for pagename in pages:
1294 # check for attachments directory
1295 page_dir = getAttachDir(request, pagename)
1296 if os.path.isdir(page_dir):
1297 # iterate over files of the page
1298 files = os.listdir(page_dir)
1299 for filename in files:
1300 filepath = os.path.join(page_dir, filename)
1301 data.addRow((
1302 Page(request, pagename).link_to(request, querystr="action=AttachFile"),
1303 wikiutil.escape(_co(request).decode(filename)),
1304 os.path.getsize(filepath),
1305 ))
1306
1307 if data:
1308 from MoinMoin.widget.browser import DataBrowserWidget
1309
1310 browser = DataBrowserWidget(request)
1311 browser.setData(data)
1312 return browser.toHTML()
1313
1314 return ''
Attached Files
To refer to attachments on a page, use attachment:filename, as shown below in the list of files. Do NOT use the URL of the [get] link, since this is subject to change and can break easily.You are not allowed to attach a file to this page.