27
27
@license: GNU GPL, see COPYING for details.
30
import os, time, zipfile, errno, datetime
31
from StringIO import StringIO
33
from werkzeug import http_date
30
import os, time, zipfile, mimetypes, errno
35
32
from MoinMoin import log
36
33
logging = log.getLogger(__name__)
38
# keep both imports below as they are, order is important:
39
from MoinMoin import wikiutil
42
from MoinMoin import config, packages
35
from MoinMoin import config, wikiutil, packages
43
36
from MoinMoin.Page import Page
44
37
from MoinMoin.util import filesys, timefuncs
45
38
from MoinMoin.security.textcha import TextCha
46
from MoinMoin.events import FileAttachedEvent, FileRemovedEvent, send_event
47
from MoinMoin.support import tarfile
39
from MoinMoin.events import FileAttachedEvent, send_event
49
41
action_name = __name__.split('.')[-1]
85
77
return u"/".join(pieces[:-1]), pieces[-1]
88
def get_action(request, filename, do):
89
generic_do_mapping = {
96
'install': action_name,
97
'upload_form': action_name,
99
basename, ext = os.path.splitext(filename)
100
do_mapping = request.cfg.extensions_mapping.get(ext, {})
101
action = do_mapping.get(do, None)
103
# we have no special support for this,
104
# look up whether we have generic support:
105
action = generic_do_mapping.get(do, None)
109
def getAttachUrl(pagename, filename, request, addts=0, do='get'):
110
""" Get URL that points to attachment `filename` of page `pagename`.
111
For upload url, call with do='upload_form'.
112
Returns the URL to do the specified "do" action or None,
113
if this action is not supported.
115
action = get_action(request, filename, do)
117
args = dict(action=action, do=do, target=filename)
118
if do not in ['get', 'view', # harmless
119
'modify', # just renders the applet html, which has own ticket
120
'move', # renders rename form, which has own ticket
122
# create a ticket for the not so harmless operations
123
# we need action= here because the current action (e.g. "show" page
124
# with a macro AttachList) may not be the linked-to action, e.g.
125
# "AttachFile". Also, AttachList can list attachments of another page,
126
# thus we need to give pagename= also.
127
args['ticket'] = wikiutil.createTicket(request,
128
pagename=pagename, action=action_name)
129
url = request.href(pagename, **args)
80
def attachUrl(request, pagename, filename=None, **kw):
81
# filename is not used yet, but should be used later to make a sub-item url
83
qs = '?%s' % wikiutil.makeQueryString(kw, want_unicode=False)
86
return "%s/%s%s" % (request.getScriptname(), wikiutil.quoteWikinameURL(pagename), qs)
89
def getAttachUrl(pagename, filename, request, addts=0, escaped=0, do='get', drawing='', upload=False):
90
""" Get URL that points to attachment `filename` of page `pagename`. """
93
url = attachUrl(request, pagename, filename,
94
rename=wikiutil.taintfilename(filename), action=action_name)
96
url = attachUrl(request, pagename, filename,
97
rename=wikiutil.taintfilename(filename), drawing=drawing, action=action_name)
100
url = attachUrl(request, pagename, filename,
101
target=filename, action=action_name, do=do)
103
url = attachUrl(request, pagename, filename,
104
drawing=drawing, action=action_name)
106
url = wikiutil.escape(url)
133
110
def getIndicator(request, pagename):
210
187
filecontent can be either a str (in memory file content),
211
188
or an open file object (file content in e.g. a tempfile).
213
192
# replace illegal chars
214
193
target = wikiutil.taintfilename(target)
216
195
# get directory, and possibly create it
217
196
attach_dir = getAttachDir(request, pagename, create=1)
218
198
fpath = os.path.join(attach_dir, target).encode(config.charset)
220
199
exists = os.path.exists(fpath)
223
remove_attachment(request, pagename, target)
225
raise AttachmentAlreadyExists
228
stream = open(fpath, 'wb')
230
_write_stream(filecontent, stream)
234
_addLogEntry(request, 'ATTNEW', pagename, target)
236
filesize = os.path.getsize(fpath)
237
event = FileAttachedEvent(request, pagename, target, filesize)
240
return target, filesize
243
def remove_attachment(request, pagename, target):
244
""" remove attachment <target> of page <pagename>
246
# replace illegal chars
247
target = wikiutil.taintfilename(target)
249
# get directory, do not create it
250
attach_dir = getAttachDir(request, pagename, create=0)
252
fpath = os.path.join(attach_dir, target).encode(config.charset)
200
if exists and not overwrite:
201
raise AttachmentAlreadyExists
208
stream = open(fpath, 'wb')
210
_write_stream(filecontent, stream)
214
_addLogEntry(request, 'ATTNEW', pagename, target)
254
216
filesize = os.path.getsize(fpath)
257
# either it is gone already or we have no rights - not much we can do about it
260
_addLogEntry(request, 'ATTDEL', pagename, target)
262
event = FileRemovedEvent(request, pagename, target, filesize)
217
event = FileAttachedEvent(request, pagename, target, filesize)
263
218
send_event(event)
265
220
return target, filesize
370
322
fmt.text(label_get) +
373
links.append(fmt.url(1, getAttachUrl(pagename, file, request, do='view')) +
374
fmt.text(label_view) +
377
if may_write and not readonly:
378
edit_url = getAttachUrl(pagename, file, request, do='modify')
380
links.append(fmt.url(1, edit_url) +
381
fmt.text(label_edit) +
326
links.append(fmt.url(1, getAttachUrl(pagename, file, request, drawing=base)) +
327
fmt.text(label_edit) +
330
links.append(fmt.url(1, getAttachUrl(pagename, file, request, do='view')) +
331
fmt.text(label_view) +
385
335
is_zipfile = zipfile.is_zipfile(fullpath)
386
if is_zipfile and not readonly:
387
337
is_package = packages.ZipPackage(request, fullpath).isPackage()
388
338
if is_package and request.user.isSuperUser():
389
339
links.append(fmt.url(1, getAttachUrl(pagename, file, request, do='install')) +
390
340
fmt.text(label_install) +
392
342
elif (not is_package and mt.minor == 'zip' and
393
may_read and may_write and may_delete):
344
request.user.may.read(pagename) and
345
request.user.may.write(pagename)):
394
346
links.append(fmt.url(1, getAttachUrl(pagename, file, request, do='unzip')) +
395
347
fmt.text(label_unzip) +
447
398
def send_link_rel(request, pagename):
448
399
files = _get_files(request, pagename)
449
400
for fname in files:
450
url = getAttachUrl(pagename, fname, request, do='view')
401
url = getAttachUrl(pagename, fname, request, do='view', escaped=1)
451
402
request.write(u'<link rel="Appendix" title="%s" href="%s">\n' % (
452
wikiutil.escape(fname, 1),
453
wikiutil.escape(url, 1)))
403
wikiutil.escape(fname), url))
406
def send_hotdraw(pagename, request):
410
pubpath = request.cfg.url_prefix_static + "/applets/TWikiDrawPlugin"
411
basename = request.form['drawing'][0]
412
drawpath = getAttachUrl(pagename, basename + '.draw', request, escaped=1)
413
pngpath = getAttachUrl(pagename, basename + '.png', request, escaped=1)
414
pagelink = attachUrl(request, pagename, '', action=action_name, ts=now)
415
helplink = Page(request, "HelpOnActions/AttachFile").url(request)
416
savelink = attachUrl(request, pagename, '', action=action_name, do='savedrawing')
417
#savelink = Page(request, pagename).url(request) # XXX include target filename param here for twisted
418
# request, {'savename': request.form['drawing'][0]+'.draw'}
419
#savelink = '/cgi-bin/dumpform.bat'
421
timestamp = '&ts=%s' % now
423
request.write('<h2>' + _("Edit drawing") + '</h2>')
426
<img src="%(pngpath)s%(timestamp)s">
427
<applet code="CH.ifa.draw.twiki.TWikiDraw.class"
428
archive="%(pubpath)s/twikidraw.jar" width="640" height="480">
429
<param name="drawpath" value="%(drawpath)s">
430
<param name="pngpath" value="%(pngpath)s">
431
<param name="savepath" value="%(savelink)s">
432
<param name="basename" value="%(basename)s">
433
<param name="viewpath" value="%(pagelink)s">
434
<param name="helppath" value="%(helplink)s">
435
<strong>NOTE:</strong> You need a Java enabled browser to edit the drawing example.
438
'pngpath': pngpath, 'timestamp': timestamp,
439
'pubpath': pubpath, 'drawpath': drawpath,
440
'savelink': savelink, 'pagelink': pagelink, 'helplink': helplink,
455
445
def send_uploadform(pagename, request):
456
446
""" Send the HTML code for the list of already stored attachments and
484
474
<input type="hidden" name="action" value="%(action_name)s">
485
475
<input type="hidden" name="do" value="upload">
486
<input type="hidden" name="ticket" value="%(ticket)s">
487
476
<input type="submit" value="%(upload_button)s">
491
'url': request.href(pagename),
480
'baseurl': request.getScriptname(),
481
'pagename': wikiutil.quoteWikinameURL(pagename),
492
482
'action_name': action_name,
493
483
'upload_label_file': _('File to upload'),
494
'upload_label_target': _('Rename to'),
495
'target': wikiutil.escape(request.values.get('target', ''), 1),
484
'upload_label_rename': _('Rename to'),
485
'rename': request.form.get('rename', [''])[0],
496
486
'upload_label_overwrite': _('Overwrite existing attachment of same name'),
497
'overwrite_checked': ('', 'checked')[request.form.get('overwrite', '0') == '1'],
487
'overwrite_checked': ('', 'checked')[request.form.get('overwrite', ['0'])[0] == '1'],
498
488
'upload_button': _('Upload'),
499
489
'textcha': TextCha(request).render(),
500
'ticket': wikiutil.createTicket(request),
503
492
request.write('<h2>' + _("Attached Files") + '</h2>')
544
536
request.theme.send_closing_html()
539
def preprocess_filename(filename):
540
""" preprocess the filename we got from upload form,
541
strip leading drive and path (IE misbehaviour)
543
if filename and len(filename) > 1 and (filename[1] == ':' or filename[0] == '\\'): # C:.... or \path... or \\server\...
544
bsindex = filename.rfind('\\')
546
filename = filename[bsindex+1:]
547
550
def _do_upload(pagename, request):
548
551
_ = request.getText
550
if not wikiutil.checkTicket(request, request.form.get('ticket', '')):
551
return _('Please use the interactive user interface to use action %(actionname)s!') % {'actionname': 'AttachFile.upload' }
553
552
# Currently we only check TextCha for upload (this is what spammers ususally do),
554
553
# but it could be extended to more/all attachment write access
555
554
if not TextCha(request).check_answer_from_form():
556
555
return _('TextCha: Wrong answer! Go back and try again...')
558
557
form = request.form
560
file_upload = request.files.get('file')
562
# This might happen when trying to upload file names
563
# with non-ascii characters on Safari.
564
return _("No file content. Delete non ASCII characters from the file name and try again.")
558
overwrite = form.get('overwrite', [u'0'])[0]
567
overwrite = int(form.get('overwrite', '0'))
560
overwrite = int(overwrite)
574
567
if overwrite and not request.user.may.delete(pagename):
575
568
return _('You are not allowed to overwrite a file attachment of this page.')
577
target = form.get('target', u'').strip()
579
target = file_upload.filename or u''
570
filename = form.get('file__filename__')
571
rename = form.get('rename', [u''])[0].strip()
577
target = preprocess_filename(target)
581
578
target = wikiutil.clean_input(target)
584
581
return _("Filename of attachment not specified!")
584
filecontent = request.form.get('file', [None])[0]
585
if filecontent is None:
586
# This might happen when trying to upload file names
587
# with non-ascii characters on Safari.
588
return _("No file content. Delete non ASCII characters from the file name and try again.")
586
590
# add the attachment
588
target, bytes = add_attachment(request, pagename, target, file_upload.stream, overwrite=overwrite)
592
target, bytes = add_attachment(request, pagename, target, filecontent, overwrite=overwrite)
589
593
msg = _("Attachment '%(target)s' (remote name '%(filename)s')"
590
594
" with %(bytes)d bytes saved.") % {
591
'target': target, 'filename': file_upload.filename, 'bytes': bytes}
595
'target': target, 'filename': filename, 'bytes': bytes}
592
596
except AttachmentAlreadyExists:
593
597
msg = _("Attachment '%(target)s' (remote name '%(filename)s') already exists.") % {
594
'target': target, 'filename': file_upload.filename}
598
'target': target, 'filename': filename}
596
600
# return attachment list
597
601
upload_form(pagename, request, msg)
601
""" A storage container (multiple objects in 1 tarfile) """
603
def __init__(self, request, pagename, containername):
604
self.request = request
605
self.pagename = pagename
606
self.containername = containername
607
self.container_filename = getFilename(request, pagename, containername)
609
def member_url(self, member):
610
""" return URL for accessing container member
611
(we use same URL for get (GET) and put (POST))
613
url = Page(self.request, self.pagename).url(self.request, {
614
'action': 'AttachFile',
615
'do': 'box', # shorter to type than 'container'
616
'target': self.containername,
619
return url + '&member=%s' % member
620
# member needs to be last in qs because twikidraw looks for "file extension" at the end
622
def get(self, member):
623
""" return a file-like object with the member file data
625
tf = tarfile.TarFile(self.container_filename)
626
return tf.extractfile(member)
628
def put(self, member, content, content_length=None):
629
""" save data into a container's member """
630
tf = tarfile.TarFile(self.container_filename, mode='a')
631
if isinstance(member, unicode):
632
member = member.encode('utf-8')
633
ti = tarfile.TarInfo(member)
634
if isinstance(content, str):
635
if content_length is None:
636
content_length = len(content)
637
content = StringIO(content) # we need a file obj
638
elif not hasattr(content, 'read'):
639
logging.error("unsupported content object: %r" % content)
641
assert content_length >= 0 # we don't want -1 interpreted as 4G-1
642
ti.size = content_length
643
tf.addfile(ti, content)
647
f = open(self.container_filename, 'w')
651
return os.path.exists(self.container_filename)
604
def _do_savedrawing(pagename, request):
607
if not request.user.may.write(pagename):
608
return _('You are not allowed to save a drawing on this page.')
610
filename = request.form['filename'][0]
611
filecontent = request.form['filepath'][0]
613
basepath, basename = os.path.split(filename)
614
basename, ext = os.path.splitext(basename)
616
# get directory, and possibly create it
617
attach_dir = getAttachDir(request, pagename, create=1)
618
savepath = os.path.join(attach_dir, basename + ext)
621
_addLogEntry(request, 'ATTDRW', pagename, basename + ext)
622
filecontent = filecontent.read() # read file completely into memory
623
filecontent = filecontent.replace("\r", "")
625
filecontent = filecontent.read() # read file completely into memory
626
filecontent = filecontent.strip()
629
# filecontent is either a file or a non-empty string
630
stream = open(savepath, 'wb')
632
_write_stream(filecontent, stream)
636
# filecontent is empty string (e.g. empty map file), delete the target file
640
if err.errno != errno.ENOENT: # no such file
643
# touch attachment directory to invalidate cache if new map is saved
645
os.utime(attach_dir, None)
647
request.emit_http_headers()
653
651
def _do_del(pagename, request):
654
652
_ = request.getText
656
if not wikiutil.checkTicket(request, request.args.get('ticket', '')):
657
return _('Please use the interactive user interface to use action %(actionname)s!') % {'actionname': 'AttachFile.del' }
659
654
pagename, filename, fpath = _access_file(pagename, request)
660
655
if not request.user.may.delete(pagename):
661
656
return _('You are not allowed to delete attachments on this page.')
663
658
return # error msg already sent in _access_file
665
remove_attachment(request, pagename, filename)
662
_addLogEntry(request, 'ATTDEL', pagename, filename)
664
if request.cfg.xapian_search:
665
from MoinMoin.search.Xapian import Index
666
index = Index(request)
668
index.remove_item(pagename, filename)
667
670
upload_form(pagename, request, msg=_("Attachment '%(filename)s' deleted.") % {'filename': filename})
712
711
if 'cancel' in request.form:
713
712
return _('Move aborted!')
714
if not wikiutil.checkTicket(request, request.form.get('ticket', '')):
715
return _('Please use the interactive user interface to use action %(actionname)s!') % {'actionname': 'AttachFile.move' }
713
if not wikiutil.checkTicket(request, request.form['ticket'][0]):
714
return _('Please use the interactive user interface to move attachments!')
716
715
if not request.user.may.delete(pagename):
717
716
return _('You are not allowed to move attachments from this page.')
719
718
if 'newpagename' in request.form:
720
new_pagename = request.form.get('newpagename')
719
new_pagename = request.form.get('newpagename')[0]
722
721
upload_form(pagename, request, msg=_("Move aborted because new page name is empty."))
723
722
if 'newattachmentname' in request.form:
724
new_attachment = request.form.get('newattachmentname')
723
new_attachment = request.form.get('newattachmentname')[0]
725
724
if new_attachment != wikiutil.taintfilename(new_attachment):
726
725
upload_form(pagename, request, msg=_("Please use a valid filename for attachment '%(filename)s'.") % {
727
726
'filename': new_attachment})
787
787
return thispage.send_page()
790
def _do_box(pagename, request):
793
pagename, filename, fpath = _access_file(pagename, request)
794
if not request.user.may.read(pagename):
795
return _('You are not allowed to get attachments from this page.')
797
return # error msg already sent in _access_file
799
timestamp = datetime.datetime.fromtimestamp(os.path.getmtime(fpath))
800
if_modified = request.if_modified_since
801
if if_modified and if_modified >= timestamp:
802
request.status_code = 304
804
ci = ContainerItem(request, pagename, filename)
805
filename = wikiutil.taintfilename(request.values['member'])
806
mt = wikiutil.MimeType(filename=filename)
807
content_type = mt.content_type()
808
mime_type = mt.mime_type()
810
# TODO: fix the encoding here, plain 8 bit is not allowed according to the RFCs
811
# There is no solution that is compatible to IE except stripping non-ascii chars
812
filename_enc = filename.encode(config.charset)
814
# for dangerous files (like .html), when we are in danger of cross-site-scripting attacks,
815
# we just let the user store them to disk ('attachment').
816
# For safe files, we directly show them inline (this also works better for IE).
817
dangerous = mime_type in request.cfg.mimetypes_xss_protect
818
content_dispo = dangerous and 'attachment' or 'inline'
821
request.headers['Date'] = http_date(now)
822
request.headers['Content-Type'] = content_type
823
request.headers['Last-Modified'] = http_date(timestamp)
824
request.headers['Expires'] = http_date(now - 365 * 24 * 3600)
825
#request.headers['Content-Length'] = os.path.getsize(fpath)
826
content_dispo_string = '%s; filename="%s"' % (content_dispo, filename_enc)
827
request.headers['Content-Disposition'] = content_dispo_string
830
request.send_file(ci.get(filename))
833
790
def _do_get(pagename, request):
834
791
_ = request.getText